Skip to content

feat(OMN-1095): Add handler source mode feature flag (bootstrap | contract | hybrid) - #187

Merged
jonahgabriel merged 16 commits into
mainfrom
jonah/omn-1095-handler-source-mode-feature-flag-bootstrap-contract-hybrid
Jan 25, 2026
Merged

jonahgabriel merged 16 commits into
mainfrom
jonah/omn-1095-handler-source-mode-feature-flag-bootstrap-contract-hybrid

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jan 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add EnumHandlerSourceMode enum with BOOTSTRAP, CONTRACT, HYBRID values
  • Add ModelHandlerSourceConfig with bootstrap expiry support
  • Implement HandlerSourceResolver with per-handler identity resolution
  • HYBRID mode: contract handler wins when identity matches, bootstrap as fallback
  • Add structured logging: contract_handler_count, bootstrap_handler_count, fallback_handler_count, override_count
  • Add validation exemptions for Handler-prefixed classes

Key Decision

Resolution is per-handler identity, not whole-source failover.

Test plan

  • TDD approach: 16 tests written first (RED), then implementation (GREEN)
  • All 16 unit tests pass
  • mypy type checking passes
  • ruff linting passes
  • Pattern validation passes (with exemptions)

Linear: https://linear.app/omninode/issue/OMN-1095

Summary by CodeRabbit

  • New Features

    • Mode-driven handler loading (BOOTSTRAP / CONTRACT / HYBRID) with a resolver, plugin-backed contract discovery, and richer resolution metrics/logging.
  • Configuration

    • New runtime config for handler source mode, bootstrap-override flag, and bootstrap expiry semantics.
  • Models

    • Handler semantic version included in contracts/loaded handlers; protocol-based handler identities using proto.* namespace.
  • Tests

    • Extensive unit/integration suites for modes, expiry, logging, and resolver behavior; legacy discovery unit tests removed.
  • Chores

    • Validation exemptions added (duplicate entries present).

✏️ Tip: You can customize this high-level summary in your review settings.

…(bootstrap | contract | hybrid)""

This reverts commit a441b7c.
@linear

linear Bot commented Jan 22, 2026

Copy link
Copy Markdown

OMN-1095

@coderabbitai

coderabbitai Bot commented Jan 22, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds handler-source selection primitives and configuration, a HandlerSourceResolver for BOOTSTRAP/CONTRACT/HYBRID merging (with per-handler override rules), runtime wiring including a PluginLoaderContractSource and proto.* identity normalization, handler_version propagation, validation exemptions, many tests (new/updated/removed), and related model/type updates.

Changes

Cohort / File(s) Summary
Enums
src/omnibase_infra/enums/enum_handler_source_mode.py, src/omnibase_infra/enums/__init__.py
Add EnumHandlerSourceMode (BOOTSTRAP, CONTRACT, HYBRID) and export it.
Handler config model
src/omnibase_infra/models/handlers/model_handler_source_config.py, src/omnibase_infra/models/handlers/__init__.py
Add ModelHandlerSourceConfig Pydantic model (mode, allow_bootstrap_override, bootstrap_expires_at) with is_bootstrap_expired and effective_mode; export it.
Handler source resolver
src/omnibase_infra/runtime/handler_source_resolver.py, src/omnibase_infra/runtime/__init__.py
Add HandlerSourceResolver implementing async resolve_handlers() for BOOTSTRAP/CONTRACT/HYBRID; performs discovery, per-handler merge using allow_bootstrap_override, aggregates validation errors, and is exported.
Runtime host integration
src/omnibase_infra/runtime/service_runtime_host_process.py
Add PluginLoaderContractSource, _load_handler_source_config(), _resolve_handler_descriptors(); rewire discovery to use HandlerSourceResolver; map handler type→kind and integrate plugin-based contract discovery.
Handler identity
src/omnibase_infra/runtime/handler_identity.py, src/omnibase_infra/runtime/__init__.py, src/omnibase_infra/runtime/handler_bootstrap_source.py
Introduce HANDLER_IDENTITY_PREFIX = "proto" and handler_identity(); switch bootstrap descriptors to proto.* identities and export identity helpers.
Model fields/version
src/omnibase_infra/models/runtime/model_handler_contract.py, src/omnibase_infra/models/runtime/model_loaded_handler.py
Add handler_version: ModelSemVer (defaults to 1.0.0 when absent in contracts) and propagate into loaded handler models.
Handler plugin loader
src/omnibase_infra/runtime/handler_plugin_loader.py
Enforce contract.handler_version presence (raise ProtocolConfigurationError if missing) and populate ModelLoadedHandler.handler_version.
Forward-reference rebuilds
src/omnibase_infra/runtime/handler_contract_source.py, src/omnibase_infra/runtime/registry_contract_source.py, src/omnibase_infra/models/handlers/__init__.py, src/omnibase_infra/models/handlers/model_contract_discovery_result.py
Centralize/explicitly call ModelContractDiscoveryResult.model_rebuild() to resolve forward refs; update explanatory comments.
Validation exemptions
src/omnibase_infra/validation/validation_exemptions.yaml
Add exemptions for enum_handler_source_mode.py and handler_source_resolver.py (OMN-1095); patch contains duplicate insertions.
Tests added/updated/removed
tests/unit/runtime/test_handler_source_mode.py, tests/integration/runtime/test_runtime_handler_source_mode.py, tests/unit/runtime/handler_plugin_loader/test_handler_plugin_loader_security.py, tests/integration/runtime/test_bootstrap_source_integration.py, tests/unit/models/handlers/test_model_bootstrap_handler_descriptor.py, tests/unit/runtime/test_handler_bootstrap_source.py, tests/unit/runtime/test_handler_discovery.py (deleted)
Add comprehensive unit/integration tests for resolver modes, precedence, expiry, logging, plugin loader security; update tests to expect proto.* IDs; remove legacy tests/unit/runtime/test_handler_discovery.py.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Resolver as HandlerSourceResolver
    participant Bootstrap as BootstrapSource
    participant Contract as ContractSource
    participant Merge as MergeLogic

    Client->>Resolver: resolve_handlers()
    alt mode == BOOTSTRAP
        Resolver->>Bootstrap: discover_handlers()
        Bootstrap-->>Resolver: bootstrap_descriptors + errors
        Resolver-->>Client: descriptors + validation_errors
    else mode == CONTRACT
        Resolver->>Contract: discover_handlers()
        Contract-->>Resolver: contract_descriptors + errors
        Resolver-->>Client: descriptors + validation_errors
    else mode == HYBRID
        Resolver->>Bootstrap: discover_handlers()
        Bootstrap-->>Resolver: bootstrap_descriptors + errors
        Resolver->>Contract: discover_handlers()
        Contract-->>Resolver: contract_descriptors + errors
        Resolver->>Merge: merge(contract_first, allow_bootstrap_override)
        Merge-->>Resolver: merged_descriptors + stats
        Resolver->>Resolver: aggregate_validation_errors()
        Resolver->>Resolver: log_resolution_stats()
        Resolver-->>Client: merged_descriptors + validation_errors
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hop through proto IDs and merge with care,

Hybrid, bootstrap, contract — I sort them fair.
Expiry ticks and overrides say who wins,
I tally counts and log each handler's skins.
A rabbit cheers: discovery finds its flair!

🚥 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 title accurately summarizes the main feature being introduced: a handler source mode feature flag with three values (bootstrap, contract, hybrid), which is the core change across the PR.
Docstring Coverage ✅ Passed Docstring coverage is 96.08% 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

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: 3

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/models/handlers/model_handler_source_config.py`:
- Around line 121-135: The is_bootstrap_expired property currently uses naive
datetime.now() which will raise TypeError when self.bootstrap_expires_at is
timezone-aware; update is_bootstrap_expired to obtain "now" that matches
bootstrap_expires_at.tzinfo: if self.bootstrap_expires_at is None return False,
otherwise if self.bootstrap_expires_at.tzinfo is not None get now via
datetime.now(tz=self.bootstrap_expires_at.tzinfo) (or use timezone.utc when
tzinfo indicates UTC), else use naive datetime.now(), then compare now >
self.bootstrap_expires_at. Ensure to reference the is_bootstrap_expired property
and bootstrap_expires_at attribute when making the change.

In `@src/omnibase_infra/runtime/handler_source_resolver.py`:
- Around line 84-103: The resolver currently ignores
ModelHandlerSourceConfig.allow_bootstrap_override; update the
HandlerSourceResolver so this flag is accepted and honored: add a parameter (or
the config object) to __init__ (the constructor shown) and store it as
self._allow_bootstrap_override, then modify the handler merging logic (the
method that merges/discovers bootstrap and contract handlers around lines
171-205 — e.g., resolve_handlers/merge_handlers/_merge logic) so when
allow_bootstrap_override is False contract-discovered handlers cannot overwrite
existing bootstrap handlers (i.e., prefer bootstrap entries), and when True
allow contract handlers to override bootstrap; also ensure callers that
construct HandlerSourceResolver pass the ModelHandlerSourceConfig or the boolean
through.

In `@tests/unit/runtime/test_handler_source_mode.py`:
- Around line 3-26: Update the module docstring in
tests/unit/runtime/test_handler_source_mode.py to reflect that
HandlerSourceResolver is implemented and tests are green: remove phrases like
"does NOT exist yet" and "should FAIL initially", change TDD/RED-phase wording
to indicate these are acceptance/unit tests that verify HYBRID resolution
behavior, and keep the listed responsibilities (Resolving handlers, per-handler
identity, contract precedence, bootstrap fallback) and test categories intact;
ensure references to HandlerSourceResolver and the test categories (Hybrid Mode
Resolution, Bootstrap Fallback, Bootstrap Only Mode, Contract Only Mode,
Structured Logging) remain accurate and phrased as current/green tests rather
than future/TDD expectations.

Comment thread src/omnibase_infra/models/handlers/model_handler_source_config.py Outdated
Comment thread src/omnibase_infra/runtime/handler_source_resolver.py
Comment thread tests/unit/runtime/test_handler_source_mode.py Outdated
Wire the HandlerSourceResolver into RuntimeHostProcess to enable mode-driven
handler discovery (BOOTSTRAP, CONTRACT, or HYBRID).

Changes:
- Add timezone-aware validator for bootstrap_expires_at (rejects naive datetimes)
- Add _load_handler_source_config() to parse mode from runtime config
- Add _resolve_handler_descriptors() using HandlerSourceResolver
- Replace sequential discovery with unified resolver-based flow
- Remove obsolete test_handler_discovery.py (superseded by resolver tests)
- Add 16 integration tests for runtime-level handler source mode behavior

Configuration:
  handler_source:
    mode: "hybrid"  # bootstrap|contract|hybrid
    bootstrap_expires_at: "2026-02-01T00:00:00Z"  # Optional, UTC required

@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/runtime/service_runtime_host_process.py`:
- Around line 982-1042: In _load_handler_source_config, parse the
allow_bootstrap_override value from the handler_source_config dict,
validate/coerce it to a boolean (accept Python bools and common truthy string
forms), and pass it into the ModelHandlerSourceConfig constructor (in addition
to handler_source_mode and bootstrap_expires_at) so the returned config reflects
the runtime setting instead of always using the default False; update any
logging to warn on invalid values and default to False when missing or
unparsable.
♻️ Duplicate comments (3)
src/omnibase_infra/models/handlers/model_handler_source_config.py (1)

105-111: allow_bootstrap_override field is defined but never honored.

The allow_bootstrap_override field is documented as controlling handler resolution in HYBRID mode, but the HandlerSourceResolver does not accept or use this flag. The field currently has no effect.

Either:

  1. Wire this flag into HandlerSourceResolver.__init__ and apply it during hybrid resolution, or
  2. Remove the field if the behavior isn't needed yet
src/omnibase_infra/runtime/handler_source_resolver.py (1)

84-102: allow_bootstrap_override flag is not wired into the resolver.

The ModelHandlerSourceConfig.allow_bootstrap_override field exists but this resolver does not accept it as a parameter. The HYBRID resolution logic (lines 171-205) always uses contract-first precedence regardless of the config setting.

This was flagged in a previous review and remains unaddressed.

src/omnibase_infra/runtime/service_runtime_host_process.py (1)

1104-1108: allow_bootstrap_override from config is not passed to the resolver.

Even if source_config.allow_bootstrap_override were parsed, it's not passed to HandlerSourceResolver. The resolver constructor doesn't accept this parameter (as flagged separately), but this is where the wiring would need to happen.

🧹 Nitpick comments (2)
tests/integration/runtime/test_runtime_handler_source_mode.py (1)

84-91: Consider using the isolated registry fixture consistently.

The isolated_handler_registry fixture creates a fresh RegistryProtocolBinding instance, but several tests (e.g., lines 209, 267, 441, 489) use get_handler_registry() singleton instead. This could cause test pollution if tests run in parallel or in a specific order.

Consider passing the isolated registry to RuntimeHostProcess via the handler_registry parameter for better test isolation.

src/omnibase_infra/runtime/service_runtime_host_process.py (1)

1091-1108: Contract source fallback may cause unexpected HYBRID behavior.

When contract_paths is empty, HandlerBootstrapSource() is used as the contract_source. In HYBRID mode, this means both sources return the same bootstrap handlers, and the resolver will see them as "contract handlers" overriding "bootstrap handlers" with the same IDs—resulting in override_count equal to the handler count and fallback_count = 0.

This is technically correct (bootstrap handlers are still loaded), but the logging and semantics may be confusing. Consider:

  1. Logging a warning when using bootstrap source as contract fallback
  2. Or documenting this edge case behavior

Comment thread src/omnibase_infra/runtime/service_runtime_host_process.py
- Change default handler_source_mode from CONTRACT to HYBRID for consistency
- Add warning log when CONTRACT mode used without contract_paths
- Add isinstance type check before handler registration
- Fix double HandlerBootstrapSource instantiation by reusing instance
- Create PluginLoaderContractSource adapter for backwards compatibility
  with simpler contract schema (handler_name, handler_class, handler_type)
- Fix protocol_type extraction to only strip bootstrap. prefix
- Update test mock to return valid ModelHandlerDescriptor
- Restore differentiated exception handling (ImportError/AttributeError vs generic)
- Tighten HYBRID mode test assertion from >= 1 to == 2 (matches comment)
- Extract PluginLoaderContractSource class to module level for testability
- Replace magic string "hybrid" with EnumHandlerSourceMode.HYBRID.value
- Use removeprefix() for clearer prefix stripping logic
CONTRACT mode now raises ProtocolConfigurationError when no contract_paths
are provided, rather than silently falling back to bootstrap handlers.

This ensures user intent is honored - CONTRACT mode means "only contract
handlers" not "contract handlers if available, bootstrap otherwise".

Changes:
- Raise ProtocolConfigurationError in _resolve_handler_descriptors() when
  CONTRACT mode is configured but no contract_paths provided
- Update test_contract_mode_raises_error_without_contract_paths
- Update test_expired_bootstrap_forces_contract_mode (expired bootstrap
  forces effective_mode to CONTRACT, which now requires paths)
…solution

Changes:
- Implement allow_bootstrap_override flag for configurable HYBRID precedence
- Add debug logging for individual handler resolution decisions
- Fix hardcoded handler_kind with proper EnumHandlerTypeCategory mapping
- Consolidate model_rebuild documentation with cross-references

The allow_bootstrap_override parameter (default False) controls which
source takes precedence in HYBRID mode conflicts. Debug logs provide
visibility into override/fallback decisions for each handler.

@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/runtime/service_runtime_host_process.py`:
- Around line 186-240: The PluginLoaderContractSource currently instantiates
HandlerPluginLoader() without namespace restrictions; update
PluginLoaderContractSource.__init__ to accept an optional allowed_namespaces:
Iterable[str] | None parameter, store it on the instance (e.g.,
self._allowed_namespaces), and pass it into
HandlerPluginLoader(allowed_namespaces=self._allowed_namespaces) when creating
self._plugin_loader; ensure any callers constructing PluginLoaderContractSource
provide the allowed_namespaces where needed and preserve default behavior when
None.
♻️ Duplicate comments (1)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

1159-1189: allow_bootstrap_override is currently ignored.
The config field isn’t parsed in _load_handler_source_config, and the resolver is instantiated without it, so the flag never affects HYBRID precedence.

🛠️ Proposed fix
@@
         if isinstance(handler_source_config, dict):
             mode_str = handler_source_config.get(
                 "mode", EnumHandlerSourceMode.HYBRID.value
             )
             expires_at_str = handler_source_config.get("bootstrap_expires_at")
+            allow_override_raw = handler_source_config.get(
+                "allow_bootstrap_override", False
+            )
@@
-            return ModelHandlerSourceConfig(
-                handler_source_mode=mode,
-                bootstrap_expires_at=expires_at,
-            )
+            if isinstance(allow_override_raw, str):
+                allow_override = (
+                    allow_override_raw.strip().lower()
+                    in {"1", "true", "yes", "on"}
+                )
+            else:
+                allow_override = bool(allow_override_raw)
+
+            return ModelHandlerSourceConfig(
+                handler_source_mode=mode,
+                bootstrap_expires_at=expires_at,
+                allow_bootstrap_override=allow_override,
+            )
@@
         resolver = HandlerSourceResolver(
             bootstrap_source=bootstrap_source,
             contract_source=contract_source,
             mode=source_config.effective_mode,
+            allow_bootstrap_override=source_config.allow_bootstrap_override,
         )

Also applies to: 1262-1267

Comment on lines +186 to +240
def __init__(self, contract_paths: list[Path]) -> None:
"""Initialize the contract source with paths to scan.

Args:
contract_paths: List of filesystem paths containing handler contracts.
"""
self._contract_paths = contract_paths
self._plugin_loader = HandlerPluginLoader()

@property
def source_type(self) -> str:
"""Return the source type identifier.

Returns:
str: Always "CONTRACT" for this filesystem-based source.
"""
return "CONTRACT"

async def discover_handlers(self) -> ModelContractDiscoveryResult:
"""Discover handlers using HandlerPluginLoader.

Scans all configured contract paths and loads handler contracts using
the HandlerPluginLoader. Each discovered handler is converted to a
ModelHandlerDescriptor for use by the handler resolution framework.

Returns:
ModelContractDiscoveryResult: Container with discovered descriptors
and any validation errors encountered during discovery.

Note:
This method uses graceful degradation - if a single contract path
fails to load, discovery continues with remaining paths and the
error is logged but not raised.
"""
# NOTE: ModelContractDiscoveryResult.model_rebuild() is called at module-level
# in handler_source_resolver.py and handler_contract_source.py to resolve
# forward references. No need to call it here - see those modules for rationale.

descriptors: list[ModelHandlerDescriptor] = []
validation_errors: list[ModelHandlerValidationError] = []

for path in self._contract_paths:
path_obj = Path(path) if isinstance(path, str) else path
if not path_obj.exists():
logger.warning(
"Contract path does not exist, skipping: %s",
path_obj,
)
continue

try:
# Use plugin loader to discover handlers with simpler schema
loaded_handlers = self._plugin_loader.load_from_directory(
directory=path_obj,
)

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:

fd -t f "handler_plugin_loader.py" -o "handler*plugin*loader*.py"

Repository: OmniNode-ai/omnibase_infra

Length of output: 239


🏁 Script executed:

fd -t f "handler_plugin_loader"

Repository: OmniNode-ai/omnibase_infra

Length of output: 365


🏁 Script executed:

wc -l src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 126


🏁 Script executed:

rg -n "def load_from_directory|allowed_namespaces|permission|stat|chmod|owner" src/omnibase_infra/runtime/handler_plugin_loader.py | head -50

Repository: OmniNode-ai/omnibase_infra

Length of output: 3599


🏁 Script executed:

sed -n '672,750p' src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 3733


🏁 Script executed:

sed -n '740,850p' src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 4473


🏁 Script executed:

rg -n "def load_from_contract" src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 101


🏁 Script executed:

sed -n '374,550p' src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 7944


🏁 Script executed:

sed -n '550,750p' src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 9132


🏁 Script executed:

rg -n "_validate_namespace\|_import_handler_class" src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 52


🏁 Script executed:

rg -n "namespace" src/omnibase_infra/runtime/handler_plugin_loader.py | head -30

Repository: OmniNode-ai/omnibase_infra

Length of output: 2455


🏁 Script executed:

sed -n '1533,1620p' src/omnibase_infra/runtime/handler_plugin_loader.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 3833


🏁 Script executed:

rg -n "class PluginLoaderContractSource" src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 131


🏁 Script executed:

sed -n '155,200p' src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 1835


🏁 Script executed:

rg -n "allowed_namespaces\|HandlerPluginLoader" src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 52


🏁 Script executed:

sed -n '155,240p' src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 3555


HandlerPluginLoader enforces file permissions, but namespace allowlisting is not configured.
The loader validates contract file access via stat operations and permission checks, which is correct. However, PluginLoaderContractSource instantiates HandlerPluginLoader() without the optional allowed_namespaces parameter, leaving namespace allowlisting disabled. If namespace-based import restrictions are required, plumb a allowed_namespaces parameter through the adapter's __init__ and pass it to the loader.

🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 186
- 240, The PluginLoaderContractSource currently instantiates
HandlerPluginLoader() without namespace restrictions; update
PluginLoaderContractSource.__init__ to accept an optional allowed_namespaces:
Iterable[str] | None parameter, store it on the instance (e.g.,
self._allowed_namespaces), and pass it into
HandlerPluginLoader(allowed_namespaces=self._allowed_namespaces) when creating
self._plugin_loader; ensure any callers constructing PluginLoaderContractSource
provide the allowed_namespaces where needed and preserve default behavior when
None.

…solution

- Add clear error handling for naive datetime in _load_handler_source_config()
  with warning about UTC requirement and example ISO formats
- Add _DEFAULT_HANDLER_KIND constant to replace hardcoded "effect" fallback
- Document HYBRID mode behavior when no contract_paths provided (intentional
  double-discovery for consistent behavior)
- Add TestHybridModeBootstrapOverride test class with 5 tests covering
  allow_bootstrap_override=True scenario
…r-source-mode-feature-flag-bootstrap-contract-hybrid
Address CodeRabbit PR review feedback:
- Parse allow_bootstrap_override from handler_source config dict
- Add boolean coercion for truthy string values (true/yes/1/on)
- Pass allow_bootstrap_override to ModelHandlerSourceConfig constructors
- Pass allow_bootstrap_override to HandlerSourceResolver
- Update test docstring to reflect GREEN phase (implementation exists)

@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/runtime/service_runtime_host_process.py`:
- Around line 257-266: The handler_id format mismatch causes duplicate entries
because bootstrap handlers use "bootstrap.{type}" while contract descriptors use
loaded.protocol_type; fix this by normalizing contract-created
ModelHandlerDescriptor.handler_id to include the bootstrap prefix (e.g., set
handler_id=f"bootstrap.{loaded.protocol_type}" where the descriptor is
constructed), so HandlerSourceResolver and handlers_by_id key lookups will
correctly detect and override bootstrap handlers.
♻️ Duplicate comments (2)
tests/unit/runtime/test_handler_source_mode.py (1)

257-258: Remove stale RED-phase note.

The resolver exists and the tests are green, so this comment is now misleading.

🧹 Suggested update
-        # Import will fail until implementation exists (TDD RED phase)
         from omnibase_infra.runtime.handler_source_resolver import HandlerSourceResolver
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

190-198: Plumb namespace allowlisting into HandlerPluginLoader.

Contracts are effectively executable. Without an allowlist, any module can be imported. Thread an optional allowed_namespaces through this adapter (and its callers) and pass it to HandlerPluginLoader so deployments can lock this down.

🔐 Suggested wiring
-from collections.abc import Awaitable, Callable
+from collections.abc import Awaitable, Callable, Iterable
@@
-    def __init__(self, contract_paths: list[Path]) -> None:
+    def __init__(
+        self,
+        contract_paths: list[Path],
+        allowed_namespaces: Iterable[str] | None = None,
+    ) -> None:
@@
-        self._plugin_loader = HandlerPluginLoader()
+        self._allowed_namespaces = allowed_namespaces
+        self._plugin_loader = HandlerPluginLoader(
+            allowed_namespaces=self._allowed_namespaces
+        )
Based on learnings, enforce namespace allowlisting for handler contracts.

Comment thread src/omnibase_infra/runtime/service_runtime_host_process.py
- Add handler_class to exception log extras for better debugging
- Document accepted truthy string values for allow_bootstrap_override
- Add debug log for HYBRID mode bootstrap source fallback behavior
- Add comment explaining deferred imports in _load_handler_source_config()

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

1409-1461: Verify HandlerSourceResolver identity matching does not normalize bootstrap handler_id format.

The removeprefix("bootstrap.") normalization at line 1415 correctly handles registration, but HandlerSourceResolver's conflict detection in HYBRID mode operates on raw handler_id values:

  • Bootstrap handlers have handler_id="bootstrap.{type}" (e.g., "bootstrap.consul")
  • Contract handlers have handler_id=loaded.protocol_type from YAML (e.g., "effect.mcp.handler")

At lines 230-235 in handler_source_resolver.py, the resolver builds lookup maps keyed by these raw handler_id strings. When it attempts conflict detection at line 277 (if descriptor.handler_id in handlers_by_id), bootstrap and contract handlers won't match due to different identifier formats. This results in both handlers being included instead of contract overriding bootstrap during per-identity resolution, defeating the HYBRID mode's precedence logic.

♻️ Duplicate comments (1)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

190-198: HandlerPluginLoader instantiated without namespace allowlisting.

Per the retrieved learnings, handler contracts are treated as executable code and should optionally use allowed_namespaces parameter for namespace allowlisting. The PluginLoaderContractSource currently creates HandlerPluginLoader() without this restriction.

If namespace-based import restrictions are required for security, consider adding an allowed_namespaces parameter to PluginLoaderContractSource.__init__ and passing it through to the loader. Based on learnings.

♻️ Suggested enhancement
-    def __init__(self, contract_paths: list[Path]) -> None:
+    def __init__(
+        self,
+        contract_paths: list[Path],
+        allowed_namespaces: tuple[str, ...] | None = None,
+    ) -> None:
         """Initialize the contract source with paths to scan.
 
         Args:
             contract_paths: List of filesystem paths containing handler contracts.
+            allowed_namespaces: Optional tuple of allowed module namespaces for
+                handler class imports. If None, all namespaces are allowed.
         """
         self._contract_paths = contract_paths
-        self._plugin_loader = HandlerPluginLoader()
+        self._plugin_loader = HandlerPluginLoader(
+            allowed_namespaces=allowed_namespaces,
+        )
🧹 Nitpick comments (2)
src/omnibase_infra/runtime/service_runtime_host_process.py (2)

257-269: Hardcoded version and model paths may not reflect actual handler metadata.

The descriptor is created with:

  • version=ModelSemVer(major=1, minor=0, patch=0) - hardcoded regardless of actual handler version
  • input_model and output_model - hardcoded string paths

If ModelLoadedHandler carries version or model information from the contract, consider using those values instead. Otherwise, this is acceptable for MVP but may need refinement when handlers declare their own versions.


1293-1295: Minor: Redundant Path conversion.

self._contract_paths is already list[Path] (converted at lines 500-501 in __init__), so the list comprehension [Path(p) for p in self._contract_paths] unnecessarily re-wraps Path objects.

♻️ Suggested simplification
             contract_source: ProtocolContractSource = PluginLoaderContractSource(
-                contract_paths=[Path(p) for p in self._contract_paths],
+                contract_paths=self._contract_paths,
             )

…space

- Fix handler ID mismatch in HYBRID mode: contract handlers now use
  bootstrap.{protocol_type} format to match bootstrap handler IDs,
  enabling proper per-identity resolution and override behavior

- Add handler_version field to ModelHandlerContract with default 1.0.0
  and propagate through ModelLoadedHandler and HandlerPluginLoader,
  replacing hardcoded version in PluginLoaderContractSource

- Wire allowed_namespaces parameter through PluginLoaderContractSource
  to HandlerPluginLoader for security namespace allowlisting

- Remove redundant Path conversion in _resolve_handler_descriptors()
  since self._contract_paths is already list[Path]

- Remove stale TDD RED phase comment in unit tests

@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/runtime/service_runtime_host_process.py`:
- Around line 239-279: discover_handlers currently treats every contract_path as
a directory and skips files; update the loop that iterates self._contract_paths
to handle file paths as well as directories by detecting path_obj.is_file() and
calling the plugin loader's file-based API (e.g.,
self._plugin_loader.load_from_file or load_from_path) for files, while
continuing to call self._plugin_loader.load_from_directory for directories;
ensure loaded_handlers is collected from whichever loader was used and the
subsequent mapping to ModelHandlerDescriptor (handler_kind mapping, handler_id,
input_model/output_model, description, handler_class, contract_path) remains
unchanged so file-backed contract descriptors are produced the same way as
directory-backed ones.

Comment on lines +239 to +279
for path in self._contract_paths:
path_obj = Path(path) if isinstance(path, str) else path
if not path_obj.exists():
logger.warning(
"Contract path does not exist, skipping: %s",
path_obj,
)
continue

try:
# Use plugin loader to discover handlers with simpler schema
loaded_handlers = self._plugin_loader.load_from_directory(
directory=path_obj,
)

# Convert ModelLoadedHandler to ModelHandlerDescriptor
for loaded in loaded_handlers:
# Map EnumHandlerTypeCategory to LiteralHandlerKind.
# handler_type is required on ModelLoadedHandler, so this always
# provides a valid value. The mapping handles COMPUTE, EFFECT,
# and NONDETERMINISTIC_COMPUTE. Falls back to "effect" for any
# unknown types as the safer option (stricter policy envelope).
handler_kind = _HANDLER_TYPE_TO_KIND.get(
loaded.handler_type, _DEFAULT_HANDLER_KIND
)

descriptor = ModelHandlerDescriptor(
# Use "bootstrap." prefix to match bootstrap handler ID format.
# This enables contract handlers to override bootstrap handlers
# with the same identity in HYBRID mode, where the resolver
# compares handler_id values for per-identity resolution.
handler_id=f"bootstrap.{loaded.protocol_type}",
name=loaded.handler_name,
version=loaded.handler_version,
handler_kind=handler_kind,
input_model="omnibase_infra.models.types.JsonDict",
output_model="omnibase_core.models.dispatch.ModelHandlerOutput",
description=f"Handler: {loaded.handler_name}",
handler_class=loaded.handler_class,
contract_path=str(loaded.contract_path),
)

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 | 🟠 Major

Handle file paths in contract discovery.
contract_paths are documented as supporting files or directories, but discover_handlers() only calls load_from_directory(). File paths will be skipped and never loaded, which breaks file-based configs.

🛠️ Suggested fix
-            try:
-                # Use plugin loader to discover handlers with simpler schema
-                loaded_handlers = self._plugin_loader.load_from_directory(
-                    directory=path_obj,
-                )
+            try:
+                # Use plugin loader to discover handlers with simpler schema
+                if path_obj.is_file():
+                    loaded_handlers = [
+                        self._plugin_loader.load_from_contract(path_obj)
+                    ]
+                elif path_obj.is_dir():
+                    loaded_handlers = self._plugin_loader.load_from_directory(
+                        directory=path_obj,
+                    )
+                else:
+                    logger.warning(
+                        "Contract path is neither file nor directory, skipping: %s",
+                        path_obj,
+                    )
+                    continue
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 239
- 279, discover_handlers currently treats every contract_path as a directory and
skips files; update the loop that iterates self._contract_paths to handle file
paths as well as directories by detecting path_obj.is_file() and calling the
plugin loader's file-based API (e.g., self._plugin_loader.load_from_file or
load_from_path) for files, while continuing to call
self._plugin_loader.load_from_directory for directories; ensure loaded_handlers
is collected from whichever loader was used and the subsequent mapping to
ModelHandlerDescriptor (handler_kind mapping, handler_id,
input_model/output_model, description, handler_class, contract_path) remains
unchanged so file-backed contract descriptors are produced the same way as
directory-backed ones.

- Replace assert statement in handler_plugin_loader.py with proper
  ProtocolConfigurationError for production safety (assert can be
  stripped with Python -O flag)
- Enhance handler_id prefix comment in PluginLoaderContractSource
  with NOTE/WARNING explaining the "bootstrap." namespace convention
  for HYBRID mode identity matching

@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/runtime/service_runtime_host_process.py`:
- Around line 1234-1255: In the except ValidationError as e block, instead of
re-raising the raw ValidationError for non-timezone issues, wrap and raise a
ProtocolConfigurationError created via
ModelInfraErrorContext.with_correlation(...).attach(...) (or the project's
pattern) so the new exception is an OnexError subclass and preserve chaining
with "raise ProtocolConfigurationError(...) from e"; keep the existing
timezone-aware branch behavior (return ModelHandlerSourceConfig(...)) and only
perform the wrapping/raising for other validation errors so correlation context
and cause are preserved.
♻️ Duplicate comments (1)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

239-253: Handle file contract paths, not just directories.
contract_paths can be files, but discovery always calls load_from_directory, so file paths are silently skipped.

🛠️ Suggested fix
-                loaded_handlers = self._plugin_loader.load_from_directory(
-                    directory=path_obj,
-                )
+                if path_obj.is_file():
+                    loaded_handlers = [
+                        self._plugin_loader.load_from_contract(path_obj)
+                    ]
+                elif path_obj.is_dir():
+                    loaded_handlers = self._plugin_loader.load_from_directory(
+                        directory=path_obj,
+                    )
+                else:
+                    logger.warning(
+                        "Contract path is neither file nor directory, skipping: %s",
+                        path_obj,
+                    )
+                    continue

Comment on lines +1234 to +1255
except ValidationError as e:
# Check if error is due to naive datetime (no timezone info)
error_messages = [err.get("msg", "") for err in e.errors()]
if any("timezone-aware" in msg for msg in error_messages):
logger.warning(
"bootstrap_expires_at must be timezone-aware (UTC recommended). "
"Naive datetime provided - falling back to no expiry. "
"Use ISO format with timezone: '2026-02-01T00:00:00+00:00' "
"or '2026-02-01T00:00:00Z'",
extra={
"invalid_value": expires_at_str,
"parsed_datetime": str(expires_at) if expires_at else None,
},
)
# Fall back to config without expiry
return ModelHandlerSourceConfig(
handler_source_mode=mode,
bootstrap_expires_at=None,
allow_bootstrap_override=allow_bootstrap_override,
)
# Re-raise other validation errors
raise

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view the specific lines mentioned
wc -l src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 133


🏁 Script executed:

# Find files with ProtocolConfigurationError and ModelInfraErrorContext
fd . -name "*.py" -type f | head -20
rg "ProtocolConfigurationError|ModelInfraErrorContext|EnumInfraTransportType" --max-count=5 -l

Repository: OmniNode-ai/omnibase_infra

Length of output: 15392


🏁 Script executed:

# Look for error class definitions
rg "class ProtocolConfigurationError|class.*InfraErrorContext|class.*EnumInfraTransportType" -A 2

Repository: OmniNode-ai/omnibase_infra

Length of output: 2685


🏁 Script executed:

# View the specific lines mentioned in the review comment
sed -n '1234,1255p' src/omnibase_infra/runtime/service_runtime_host_process.py | cat -n

Repository: OmniNode-ai/omnibase_infra

Length of output: 1447


🏁 Script executed:

# Get more context around those lines to understand the function
sed -n '1220,1260p' src/omnibase_infra/runtime/service_runtime_host_process.py | cat -n

Repository: OmniNode-ai/omnibase_infra

Length of output: 2256


🏁 Script executed:

# Check the EnumInfraTransportType enum values
cat src/omnibase_infra/enums/enum_infra_transport_type.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 2840


🏁 Script executed:

# Check ModelInfraErrorContext and its with_correlation method
cat src/omnibase_infra/models/errors/model_infra_error_context.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 3750


🏁 Script executed:

# Check ProtocolConfigurationError class definition and usage
cat src/omnibase_infra/errors/error_infra.py | head -150

Repository: OmniNode-ai/omnibase_infra

Length of output: 6114


🏁 Script executed:

# Look for error chaining patterns with ProtocolConfigurationError in the codebase
rg "raise ProtocolConfigurationError" -B 2 -A 2 | head -50

Repository: OmniNode-ai/omnibase_infra

Length of output: 3942


🏁 Script executed:

# Verify imports in service_runtime_host_process.py to see what's already available
head -50 src/omnibase_infra/runtime/service_runtime_host_process.py

Repository: OmniNode-ai/omnibase_infra

Length of output: 1848


🏁 Script executed:

# Find the function signature that contains the problematic code
rg "def.*load_handler_source_config|def.*bootstrap_expires_at" src/omnibase_infra/runtime/service_runtime_host_process.py -B 2 -A 15

Repository: OmniNode-ai/omnibase_infra

Length of output: 859


🏁 Script executed:

# Get the full function definition
sed -n '1200,1280p' src/omnibase_infra/runtime/service_runtime_host_process.py | cat -n

Repository: OmniNode-ai/omnibase_infra

Length of output: 4208


🏁 Script executed:

# Check the imports section more thoroughly
sed -n '1,100p' src/omnibase_infra/runtime/service_runtime_host_process.py | cat -n

Repository: OmniNode-ai/omnibase_infra

Length of output: 4472


🏁 Script executed:

# Look for existing error handling patterns in this file
rg "raise.*Error|ProtocolConfigurationError|ModelInfraErrorContext" src/omnibase_infra/runtime/service_runtime_host_process.py -B 2 -A 2

Repository: OmniNode-ai/omnibase_infra

Length of output: 3649


Wrap ValidationError in ProtocolConfigurationError with proper error chaining.

Re-raising ValidationError directly violates the mandatory OnexError-only rule and drops correlation context for non-timezone validation errors, breaking distributed tracing. All exceptions in this codebase must be OnexError subclasses with raise ... from e chaining and ModelInfraErrorContext.with_correlation() for infrastructure operations.

Fix
-                # Re-raise other validation errors
-                raise
+                context = ModelInfraErrorContext.with_correlation(
+                    transport_type=EnumInfraTransportType.RUNTIME,
+                    operation="load_handler_source_config",
+                )
+                raise ProtocolConfigurationError(
+                    "Invalid handler_source configuration",
+                    context=context,
+                ) from e
🤖 Prompt for AI Agents
In `@src/omnibase_infra/runtime/service_runtime_host_process.py` around lines 1234
- 1255, In the except ValidationError as e block, instead of re-raising the raw
ValidationError for non-timezone issues, wrap and raise a
ProtocolConfigurationError created via
ModelInfraErrorContext.with_correlation(...).attach(...) (or the project's
pattern) so the new exception is an OnexError subclass and preserve chaining
with "raise ProtocolConfigurationError(...) from e"; keep the existing
timezone-aware branch behavior (return ModelHandlerSourceConfig(...)) and only
perform the wrapping/raising for other validation errors so correlation context
and cause are preserved.

Address code review feedback with 3 improvements:

1. Handler ID semantics (neutral prefix + shared helper)
   - Create handler_identity.py with shared handler_identity() function
   - Change prefix from "bootstrap." to "proto." (identity namespace, not source)
   - Both bootstrap and contract sources now use shared function
   - Prevents confusion about handler source vs identity

2. Add namespace allowlist security tests (10 new tests)
   - test_rejects_handler_outside_allowed_namespace
   - test_accepts_handler_inside_allowed_namespace
   - test_rejects_before_import_prevents_side_effects
   - test_prevents_underscore_boundary_bypass
   - test_prevents_similar_name_typosquatting
   - Tests verify NAMESPACE_NOT_ALLOWED (HANDLER_LOADER_013) error

3. Document HYBRID double-discovery design decision
   - Add detailed docstring explaining WHY double-discovery is intentional
   - Add inline warning against "optimizing" to single call
   - Document observability symmetry and semantic correctness reasons

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/runtime/test_handler_bootstrap_source.py (2)

183-219: Docstring still references old "bootstrap." prefix pattern.

Lines 187-192 describe handler IDs as bootstrap.consul, bootstrap.db, etc., but the actual expected IDs are now proto.*. The docstring should be updated for consistency.

📝 Update docstring to reflect proto.* prefix
     async def test_discovers_exactly_five_handlers(self) -> None:
         """discover_handlers() should return exactly 5 bootstrap handlers.

         The bootstrap handlers are:
-        - bootstrap.consul: HashiCorp Consul service discovery
-        - bootstrap.db: PostgreSQL database operations
-        - bootstrap.http: HTTP REST protocol
-        - bootstrap.mcp: Model Context Protocol for AI agent integration
-        - bootstrap.vault: HashiCorp Vault secret management
+        - proto.consul: HashiCorp Consul service discovery
+        - proto.db: PostgreSQL database operations
+        - proto.http: HTTP REST protocol
+        - proto.mcp: Model Context Protocol for AI agent integration
+        - proto.vault: HashiCorp Vault secret management
         """

203-219: Docstring references old "bootstrap." prefix pattern.

Line 207-208 states Handler IDs must follow the pattern "bootstrap.<service_name>" but the test now expects proto.* IDs.

📝 Update docstring pattern description
     async def test_discovered_handler_ids_match_expected(self) -> None:
         """All discovered handlers should have expected handler_id values.

-        Handler IDs must follow the pattern "bootstrap.<service_name>" where
+        Handler IDs must follow the pattern "proto.<service_name>" where
         service_name is one of: consul, db, http, mcp, vault.
         """
♻️ Duplicate comments (2)
src/omnibase_infra/runtime/service_runtime_host_process.py (2)

243-256: Handle file-based contract paths.
contract_paths are documented to accept files, but discovery always calls load_from_directory(), so file paths will be skipped.

🛠️ Proposed fix
-                loaded_handlers = self._plugin_loader.load_from_directory(
-                    directory=path_obj,
-                )
+                if path_obj.is_file():
+                    loaded_handlers = [
+                        self._plugin_loader.load_from_contract(path_obj)
+                    ]
+                elif path_obj.is_dir():
+                    loaded_handlers = self._plugin_loader.load_from_directory(
+                        directory=path_obj,
+                    )
+                else:
+                    logger.warning(
+                        "Contract path is neither file nor directory, skipping: %s",
+                        path_obj,
+                    )
+                    continue

1232-1260: Wrap ValidationError in ProtocolConfigurationError with chaining.
Re‑raising ValidationError breaks the OnexError‑only rule and drops correlation context.

🛠️ Proposed fix
-                # Re-raise other validation errors
-                raise
+                context = ModelInfraErrorContext.with_correlation(
+                    transport_type=EnumInfraTransportType.RUNTIME,
+                    operation="load_handler_source_config",
+                )
+                raise ProtocolConfigurationError(
+                    "Invalid handler_source configuration",
+                    context=context,
+                ) from e
As per coding guidelines, only OnexError subclasses should be raised.
🧹 Nitpick comments (6)
src/omnibase_infra/runtime/handler_identity.py (1)

45-75: Consider adding input validation for protocol_type.

The function accepts any string, including empty strings or strings with invalid characters. This could produce malformed handler IDs like "proto." or "proto.foo.bar".

♻️ Optional: Add minimal validation
 def handler_identity(protocol_type: str) -> str:
+    if not protocol_type or "." in protocol_type:
+        raise ValueError(
+            f"protocol_type must be a non-empty string without dots, got: {protocol_type!r}"
+        )
     return f"{HANDLER_IDENTITY_PREFIX}.{protocol_type}"

Alternatively, if callers are trusted internal code, document the expected format in the docstring Args section (e.g., "must be a simple identifier like 'consul', 'http'").

src/omnibase_infra/runtime/handler_bootstrap_source.py (1)

429-438: Verify exception handling doesn't suppress useful context.

The bare raise preserves the original ProtocolConfigurationError, but the preceding logger.exception() already logs the traceback. This is fine, but consider whether the log level should be ERROR instead of using exception() which implies ERROR + traceback.

The current approach is acceptable since fail-fast is the intended behavior for bootstrap handlers. No change needed.

tests/unit/runtime/handler_plugin_loader/test_handler_plugin_loader_security.py (3)

300-324: Good boundary bypass test, but accesses private method directly.

The test calls loader._validate_namespace() which is a private method (underscore prefix). While this provides precise testing of the validation logic, consider whether this creates coupling to implementation details.

If the public API (load_from_contract) provides sufficient coverage, testing through that interface is more robust against refactoring. However, for security boundary tests, direct testing of the validation function is acceptable to ensure the exact behavior.


351-380: Clarify expected behavior in docstring.

The docstring states that "omnibase" (without period) should match "omnibase.handlers.Auth" but NOT "omnibase_core.handlers.Handler". This is the correct behavior, but the test name test_prevents_namespace_without_period_from_matching_extended_names is slightly confusing since "extended names" could be misread.

The test logic is correct. The current name works, but consider renaming to test_namespace_without_period_enforces_dot_boundary for clarity.


146-195: Remove unnecessary @pytest.mark.asyncio decorator.

The load_from_contract() method is synchronous (defined as def not async def in handler_plugin_loader.py:374), and this test contains no await statements or other async operations.

♻️ Consider making this a sync test
-    `@pytest.mark.asyncio`
-    async def test_accepts_handler_inside_allowed_namespace(
+    def test_accepts_handler_inside_allowed_namespace(
         self,
         tmp_path: Path,
     ) -> None:
tests/integration/runtime/test_bootstrap_source_integration.py (1)

238-312: Well-documented HYBRID mode double-discovery test.

The docstring clearly explains the intentional design decision: discover_handlers() is called twice in HYBRID mode for observability symmetry and semantic correctness. The warning against "optimizing" this behavior is valuable for future maintainers.

However, the inline imports at lines 259-260 could be moved to the module level for consistency.

♻️ Consider moving imports to module level
 from omnibase_infra.runtime.service_runtime_host_process import RuntimeHostProcess
+from omnibase_core.models.primitives import ModelSemVer
+from omnibase_infra.models.handlers import ModelHandlerDescriptor
 
 # ...
 
     async def test_bootstrap_source_called_during_start(self) -> None:
         # ...
-        from omnibase_core.models.primitives import ModelSemVer
-        from omnibase_infra.models.handlers import ModelHandlerDescriptor

Inline imports in test methods are acceptable but can make import analysis harder. Since these are standard project models, module-level imports are cleaner.

… metric

Address code review feedback with two improvements:

1. ModelHandlerSourceConfig: Add _coerce_allow_bootstrap_override field
   validator to handle string-to-bool coercion (true/yes/1/on) at the
   Pydantic layer rather than in runtime config loading

2. HandlerSourceResolver: Add validation_error_count metric to HYBRID
   mode logging for complete observability of error aggregation
…_config

The manual bool coercion for allow_bootstrap_override was redundant since
ModelHandlerSourceConfig._coerce_allow_bootstrap_override field validator
(mode="before") already handles string-to-bool coercion at the Pydantic layer.

This simplifies the code by passing the raw value directly to the model
and letting the field validator handle coercion uniformly.
Code review follow-up improvements:

- Add explicit int/float handling in _coerce_allow_bootstrap_override
  validator (0/0.0 → False, other numbers → True, unknown types → False)
- Document forward reference resolution pattern for
  ModelContractDiscoveryResult in __init__.py and model docstrings
- Ensure model_rebuild() called at module level in all runtime modules
  that create ModelContractDiscoveryResult instances (idempotent pattern
  due to circular import constraint)
- Clean up redundant code in handler_bootstrap_source.py and
  handler_contract_source.py

@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: 2

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/models/handlers/model_handler_source_config.py`:
- Around line 152-180: The string-to-bool parsing should strip whitespace before
lowercasing (change the str branch to call value.strip().lower()) and the
_validate_expires_at_timezone validator must raise the project's OnexError (or
an appropriate OnexError subclass) instead of ValueError when value.tzinfo is
None; update the exception type and message accordingly and ensure OnexError is
imported where _validate_expires_at_timezone is defined (reference symbols: the
string-to-bool branch at the top of the snippet and the
_validate_expires_at_timezone validator for bootstrap_expires_at).

In `@tests/unit/runtime/test_handler_contract_source.py`:
- Around line 953-958: Update the comment that currently claims "model_rebuild()
is called centrally in models.handlers.__init__" to accurately state that
forward-reference resolution (model_rebuild) is performed by the runtime
modules, not in omnibase_infra.models.handlers.__init__ which explicitly avoids
it; adjust the two comment occurrences referencing ModelContractDiscoveryResult
and model_rebuild to say the forward reference is resolved centrally in the
runtime modules so future readers are not misled.

Comment on lines +152 to +180
if isinstance(value, str):
return value.lower() in ("true", "yes", "1", "on")
if isinstance(value, (int, float)):
# Explicit: 0/0.0 = False, any other number = True
return bool(value)
# Unknown types default to False for safety
return False

@field_validator("bootstrap_expires_at")
@classmethod
def _validate_expires_at_timezone(cls, value: datetime | None) -> datetime | None:
"""Validate and normalize bootstrap_expires_at to UTC.

Args:
value: The datetime value to validate.

Returns:
None if value is None, otherwise the datetime normalized to UTC.

Raises:
ValueError: If the datetime is naive (no timezone info).
"""
if value is None:
return None
if value.tzinfo is None:
raise ValueError(
"bootstrap_expires_at must be timezone-aware (UTC recommended). "
"Use datetime.now(timezone.utc) or datetime(..., tzinfo=timezone.utc)."
)

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

Use OnexError for expiry validation; trim boolean strings.
Line 176 raises ValueError, which conflicts with the repo rule to raise only OnexError subclasses. Also, Line 152 would be more robust if it stripped whitespace before lowercasing. As per coding guidelines.

🛠️ Proposed fix
-from datetime import UTC, datetime, timezone
+from datetime import UTC, datetime, timezone

 from pydantic import BaseModel, ConfigDict, Field, field_validator
 
+from omnibase_core.models.errors.model_onex_error import ModelOnexError
 from omnibase_infra.enums.enum_handler_source_mode import EnumHandlerSourceMode
@@
         if isinstance(value, bool):
             return value
         if isinstance(value, str):
-            return value.lower() in ("true", "yes", "1", "on")
+            return value.strip().lower() in ("true", "yes", "1", "on")
@@
         if value.tzinfo is None:
-            raise ValueError(
-                "bootstrap_expires_at must be timezone-aware (UTC recommended). "
-                "Use datetime.now(timezone.utc) or datetime(..., tzinfo=timezone.utc)."
-            )
+            raise ModelOnexError(
+                "bootstrap_expires_at must be timezone-aware (UTC recommended). "
+                "Use datetime.now(timezone.utc) or datetime(..., tzinfo=timezone.utc).",
+                error_code="HANDLER_SOURCE_CONFIG_001",
+            )
🤖 Prompt for AI Agents
In `@src/omnibase_infra/models/handlers/model_handler_source_config.py` around
lines 152 - 180, The string-to-bool parsing should strip whitespace before
lowercasing (change the str branch to call value.strip().lower()) and the
_validate_expires_at_timezone validator must raise the project's OnexError (or
an appropriate OnexError subclass) instead of ValueError when value.tzinfo is
None; update the exception type and message accordingly and ensure OnexError is
imported where _validate_expires_at_timezone is defined (reference symbols: the
string-to-bool branch at the top of the snippet and the
_validate_expires_at_timezone validator for bootstrap_expires_at).

Comment on lines +953 to +958
This test imports ModelContractDiscoveryResult - the forward reference is
resolved centrally in omnibase_infra.models.handlers.__init__.
"""
# Import through handler_contract_source which calls model_rebuild()
# Import models - model_rebuild() is called centrally in models.handlers.__init__
from omnibase_infra.models.errors import ModelHandlerValidationError
from omnibase_infra.runtime.handler_contract_source import (
ModelContractDiscoveryResult,
)
from omnibase_infra.models.handlers import ModelContractDiscoveryResult

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

Align forward-reference comments with actual rebuild location.
The comments say model_rebuild is centralized in models.handlers.init, but that module explicitly avoids it; runtime modules handle resolution. Updating the wording prevents confusion.

📝 Suggested wording
-        resolved centrally in omnibase_infra.models.handlers.__init__.
+        resolved in runtime modules that import ModelHandlerValidationError
+        (e.g., handler_contract_source.py, registry_contract_source.py).
@@
-        # Import models - model_rebuild() is called centrally in models.handlers.__init__
+        # Import models - model_rebuild() is called in runtime modules
+        # (e.g., handler_contract_source.py)

Also applies to: 1007-1008

🤖 Prompt for AI Agents
In `@tests/unit/runtime/test_handler_contract_source.py` around lines 953 - 958,
Update the comment that currently claims "model_rebuild() is called centrally in
models.handlers.__init__" to accurately state that forward-reference resolution
(model_rebuild) is performed by the runtime modules, not in
omnibase_infra.models.handlers.__init__ which explicitly avoids it; adjust the
two comment occurrences referencing ModelContractDiscoveryResult and
model_rebuild to say the forward reference is resolved centrally in the runtime
modules so future readers are not misled.

@jonahgabriel
jonahgabriel merged commit 5234951 into main Jan 25, 2026
19 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-1095-handler-source-mode-feature-flag-bootstrap-contract-hybrid branch January 25, 2026 15:03
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