Skip to content

feat(OMN-1613): Add event bus topic storage to registry for dynamic topic routing - #199

Merged
jonahgabriel merged 14 commits into
mainfrom
jonah/omn-1613-add-event-bus-topic-storage-to-registry-for-dynamic-topic
Jan 27, 2026
Merged

jonahgabriel merged 14 commits into
mainfrom
jonah/omn-1613-add-event-bus-topic-storage-to-registry-for-dynamic-topic

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Jan 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Enable registry-driven topic routing by storing event_bus configuration (subscribe_topics, publish_topics) as part of node introspection data in Consul KV.

  • Add event_bus field to ModelNodeIntrospectionEvent containing resolved, environment-qualified topic strings
  • Store topic configuration in Consul KV during node registration
  • Create reverse index (onex/topics/{topic}/subscribers) for topic → node_id lookup
  • Add get_subscribers_for_topic() to RuntimeHostProcess for dynamic routing queries

Key Changes

Models

  • ModelEventBusTopicEntry - Single topic entry with optional tooling metadata
  • ModelNodeEventBusConfig - Container for subscribe/publish topic lists with subscribe_topic_strings / publish_topic_strings properties

Introspection

  • _extract_event_bus_config() in MixinNodeIntrospection - Extracts topics from contract and resolves env prefix
  • Fail-fast on unresolved placeholders ({env}, {namespace})

Consul Storage

  • MixinConsulTopicIndex - New mixin for topic index management
  • Idempotent index updates (delta computation between old/new topics)
  • Separate storage for routing (topic strings) vs tooling (full entries)

Runtime

  • get_subscribers_for_topic(topic: str) -> list[UUID] - Query Consul for nodes subscribing to a topic

Design Decisions (LOCKED)

  1. Topics stored as environment-qualified strings (e.g., dev.onex.evt.intent-classified.v1)
  2. Consul KV is the authoritative routing index - PostgreSQL is for audit only
  3. Index updates are idempotent - Safe for restarts/retries
  4. Metadata is tooling-only - Routing uses only topic strings, never metadata fields
  5. Fail-fast on unresolved placeholders - Misconfigured topics caught at introspection time

Test Plan

  • 88 unit tests across 6 new test files
  • 33 integration tests across 2 new test files
  • Verify introspection event contains resolved event_bus
  • Verify Consul KV has correct structure (onex/nodes/{node_id}/event_bus/, onex/topics/{topic}/subscribers)
  • Verify get_subscribers_for_topic() returns correct node UUIDs
  • Verify idempotency (re-registration doesn't create duplicates)

Related

  • Ticket: OMN-1613
  • Related: OMN-1550 (Local topic resolution), OMN-1547 (Replace hardcoded topics), OMN-1518 (Declarative topic→handler routing)

Summary by CodeRabbit

  • New Features

    • Introspection events include resolved environment-qualified event-bus configurations.
    • Registry-backed topic index for dynamic topic-based routing with idempotent subscriber management.
    • Runtime API to query subscribers for a given topic.
  • Tests

    • Extensive new unit and integration tests covering extraction, storage, indexing, idempotency, routing queries, and runtime topic lookup.

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

…opic routing

Enable registry-driven topic routing by storing event_bus configuration
(subscribe_topics, publish_topics) as part of node introspection data
in Consul KV.

Key changes:
- Add ModelEventBusTopicEntry and ModelNodeEventBusConfig models
- Add event_bus field to ModelNodeIntrospectionEvent
- Add _extract_event_bus_config() to MixinNodeIntrospection
- Add MixinConsulTopicIndex for topic index management in Consul KV
- Add get_subscribers_for_topic() to RuntimeHostProcess
- Update RegistrationReducer to pass through event_bus

Design decisions:
- Topics stored as environment-qualified strings (e.g., dev.onex.evt...)
- Consul KV is the authoritative routing index
- Index updates are idempotent (safe for restarts/retries)
- Metadata is tooling-only; routing uses only topic strings
- Fail-fast on unresolved placeholders

Test coverage:
- 88 unit tests across 6 test files
- 33 integration tests across 2 test files
@coderabbitai

coderabbitai Bot commented Jan 27, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds event-bus models and introspection-based topic resolution, a Consul KV-backed topic→subscriber index mixin integrated into service registration, a runtime API to query subscribers, and extensive unit/integration tests for extraction, storage, idempotency, and queries.

Changes

Cohort / File(s) Summary
Event Bus Models
src/omnibase_infra/models/registration/model_event_bus_topic_entry.py, src/omnibase_infra/models/registration/model_node_event_bus_config.py
New immutable Pydantic models ModelEventBusTopicEntry and ModelNodeEventBusConfig with properties to extract topic strings.
Model Exports & Introspection Event
src/omnibase_infra/models/registration/__init__.py, src/omnibase_infra/models/registration/model_node_introspection_event.py
Exposed new models and added optional `event_bus: ModelNodeEventBusConfig
Node Introspection Mixins
src/omnibase_infra/mixins/mixin_node_introspection.py
Added _extract_event_bus_config(env_prefix) and wired resolved event_bus into get_introspection_data (env-prefix resolution, validation, fail-fast on unresolved placeholders).
Consul Topic Index Mixin
src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py, src/omnibase_infra/handlers/mixins/__init__.py
New MixinConsulTopicIndex: Consul KV helpers with retry, per-node event-bus storage, idempotent add/remove subscriber ops, delta-based topic index updates, and exports.
Consul Service Mixin & Handler
src/omnibase_infra/handlers/mixins/mixin_consul_service.py, src/omnibase_infra/handlers/handler_consul.py
Added _parse_event_bus_config, declared _store_node_event_bus and _update_topic_index dependency methods, updated _register_service to process event_bus_config, and inserted MixinConsulTopicIndex into HandlerConsul base classes and imports.
Registration Payload & Reducer
src/omnibase_infra/nodes/reducers/models/model_payload_consul_register.py, src/omnibase_infra/nodes/reducers/registration_reducer.py
Added optional event_bus_config to payload model and propagated event.event_bus into Consul registration payload.
Runtime API
src/omnibase_infra/runtime/service_runtime_host_process.py
New async get_subscribers_for_topic(topic: str) -> list[UUID] reading onex/topics/{topic}/subscribers from Consul KV, parsing UUIDs, and returning [] on missing/malformed data or Consul errors.
Topic Name Pattern Constant
src/omnibase_infra/constants_topic_patterns.py
New TOPIC_NAME_PATTERN regex enforcing topic token characters for validation.
Handler Imports & Exports
src/omnibase_infra/handlers/mixins/__init__.py, src/omnibase_infra/handlers/handler_consul.py
Added MixinConsulTopicIndex to mixins package exports and to HandlerConsul MRO before MixinConsulService.
Tests — Integration & Unit
tests/integration/registration/..., tests/integration/routing/..., tests/unit/... (many files)
Large test additions covering extraction, model behavior, Consul KV storage and format, reverse-index idempotency, runtime topic queries, end-to-end registration→indexing flows, and supporting fixtures/conftest re-exports.

Sequence Diagram(s)

sequenceDiagram
    participant Node as Node Introspection
    participant Contract as Contract Spec
    participant Extract as _extract_event_bus_config()
    participant Reducer as RegistrationReducer
    participant Handler as HandlerConsul (Service + TopicIndex)
    participant ConsulKV as Consul KV

    Node->>Contract: read event_bus subcontract
    Node->>Extract: resolve topics with env_prefix
    Extract-->>Node: ModelNodeEventBusConfig
    Node->>Reducer: introspection event (includes event_bus)
    Reducer->>Handler: build ModelPayloadConsulRegister (event_bus_config)
    Handler->>ConsulKV: _update_topic_index() (get previous subscribers)
    ConsulKV-->>Handler: previous subscriber lists
    Handler->>ConsulKV: add/remove subscriber entries (onex/topics/{topic}/subscribers)
    Handler->>ConsulKV: _store_node_event_bus() (per-node keys)
Loading
sequenceDiagram
    participant App as Caller
    participant Runtime as RuntimeHostProcess
    participant ConsulHandler as Consul Handler
    participant ConsulKV as Consul KV

    App->>Runtime: get_subscribers_for_topic(topic)
    Runtime->>ConsulHandler: GET onex/topics/{topic}/subscribers
    ConsulHandler->>ConsulKV: retrieve key
    ConsulKV-->>ConsulHandler: JSON array or missing
    ConsulHandler-->>Runtime: raw payload or None/errors
    Runtime->>Runtime: parse JSON, validate UUIDs
    Runtime-->>App: list[UUID] or []
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • omnibase_infra#54: Overlaps with MixinNodeIntrospection and event-bus/topic model changes; likely touches extraction and model types.
  • omnibase_infra#197: Modifies RuntimeHostProcess-related behavior and may overlap with the new get_subscribers_for_topic additions.

Poem

🐰 I found new topics in the ground,
I hopped through KV where keys abound.
Introspection stitched their names just right,
Consul keeps track through day and night.
Hooray—subscribers bloom in sight! 🎉

🚥 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 PR title clearly and concisely describes the main feature: adding event bus topic storage to the registry to enable dynamic topic routing.
Docstring Coverage ✅ Passed Docstring coverage is 96.14% 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: 5

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/handlers/mixins/mixin_consul_service.py`:
- Around line 153-209: The _parse_event_bus_config function should validate
topic entries and accept a correlation_id and a JsonType payload: change the
signature to accept event_bus_data: JsonType and correlation_id: str, and inside
both subscribe and publish loops validate that entry is a dict and that
entry.get("topic") is a non-empty string (not coerced via str(...)); if invalid,
raise a typed validation error (e.g., PayloadValidationError or ValueError if no
custom error exists) including the correlation_id and a clear message, and only
construct ModelEventBusTopicEntry when topic is valid; ensure the same checks
are applied for both subscribe_topics and publish_topics before returning the
ModelNodeEventBusConfig.
- Around line 316-336: The handler currently ignores cases where payload
contains event_bus_config but node_id is missing/invalid; change the logic in
the block that reads payload.get("event_bus_config") and payload.get("node_id")
to fail fast by raising a RuntimeHostError with context when event_bus_config is
a dict but node_id is not a non-empty string. Specifically, before calling
self._parse_event_bus_config, validate node_id and if invalid raise
RuntimeHostError including correlation_id and a clear message about the
missing/invalid node_id so misconfigured registrations are surfaced rather than
silently dropped; keep subsequent calls to _parse_event_bus_config,
_update_topic_index, and _store_node_event_bus unchanged when node_id is valid.

In `@src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py`:
- Around line 313-317: JSON parsing of Consul values using json.loads (e.g.,
when reading previous_result into old_topics around previous_key and wherever
json.loads is used in _update_topic_index and related methods) can raise
JSONDecodeError/TypeError and currently bubbles as a raw ValueError; wrap every
json.loads call in a try/except that catches
ValueError/TypeError/JSONDecodeError and re-raise as InfraConsulError including
the consul key (e.g., previous_key or the specific consul_key variable) and the
correlation_id, preserving exception chaining (raise InfraConsulError(...,
consul_key=consul_key, correlation_id=correlation_id) from e); apply this
pattern to the json.loads usages referenced (around
previous_key/event_bus.subscribe_topic_strings and the blocks at the other noted
locations) so the error contract uses OnexError subclasses and retains trace
context.

In `@src/omnibase_infra/mixins/mixin_node_introspection.py`:
- Around line 1444-1453: Replace the plain ValueError re-raise with the
project's structured error type: catch the ValueError from
self._extract_event_bus_config and raise ProtocolConfigurationError (a subclass
of OnexError) instead, preserving the original message and chaining the original
exception via "from e"; also add (or update) the import for
ProtocolConfigurationError at the top of the module so the symbol is available.

In `@tests/integration/registration/test_introspection_event_bus_e2e.py`:
- Around line 681-704: The v1 registration currently calls
handler._store_node_event_bus before handler._update_topic_index which reverses
the intended sequence used for delta computation; change the v1 block to call
handler._update_topic_index(node_id=unique_node_id, event_bus=config_v1,
correlation_id=correlation_id) first and then handler._store_node_event_bus(...)
so it matches the v2 ordering (update index FIRST, then store SECOND) and aligns
with the comment about reading previous state.
🧹 Nitpick comments (1)
tests/integration/registration/test_introspection_event_bus_e2e.py (1)

342-351: Consider using specific exception type for clarity.

The test catches generic Exception for the frozen model mutation check. Using pydantic.ValidationError would make the test more precise and self-documenting.

Proposed refinement
+from pydantic import ValidationError
+
 def test_event_bus_config_immutability(self) -> None:
     """ModelNodeEventBusConfig is frozen (immutable)."""
     config = ModelNodeEventBusConfig(
         subscribe_topics=[ModelEventBusTopicEntry(topic="dev.test.v1")],
     )

     # Attempting to modify should raise
-    with pytest.raises(Exception):  # ValidationError for frozen model
+    with pytest.raises(ValidationError):
         config.subscribe_topics = []  # type: ignore[misc]

Comment thread src/omnibase_infra/handlers/mixins/mixin_consul_service.py
Comment thread src/omnibase_infra/handlers/mixins/mixin_consul_service.py
Comment thread src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py
Comment thread src/omnibase_infra/mixins/mixin_node_introspection.py Outdated
Comment thread tests/integration/registration/test_introspection_event_bus_e2e.py
…ting

Code quality improvements from PR review:

- Add warning logs for empty/missing topic entries in _parse_event_bus_config
- Add specific exception handlers (InfraConsulError, InfraTimeoutError,
  InfraUnavailableError) before broad Exception catch in get_subscribers_for_topic
- Document non-atomic read-modify-write limitation in _update_topic_index
  with production recommendations (Consul transactions, optimistic locking)

@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 2297-2331: The current list comprehension [UUID(nid) for nid in
node_ids_raw if isinstance(nid, str)] will raise ValueError on the first invalid
UUID and cause all subscribers to be dropped; replace it with an explicit loop
over node_ids_raw (checking isinstance(nid, str)), attempt to convert each nid
with UUID(nid) inside a try/except ValueError, append successful conversions to
a new list and skip (and optionally logger.warning) invalid entries so valid
UUIDs are preserved, then return that filtered list (also remove or adjust the
outer ValueError handler that currently returns [] for any conversion error).
Use the existing variables node_ids_raw, topic, correlation_id, and logger to
provide contextual warnings for skipped invalid IDs.

Comment thread src/omnibase_infra/runtime/service_runtime_host_process.py
pytest 8+ no longer supports defining pytest_plugins in non-top-level
conftest files. The directive now raises an error instead of a warning.

Changes:
- Move pytest_plugins from tests/integration/registration/conftest.py
  to tests/conftest.py (root level)
- Update docstrings to document fixture availability

This fixes the "2 errors" seen in all CI test splits:
  ERROR collecting tests/integration/registration
  Defining 'pytest_plugins' in a non-top-level conftest is no longer supported

Reference:
https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files
…egistration

pytest_plugins cannot be used for conftest.py files that are already in
the test directory tree - pytest automatically discovers them during
collection, causing "Plugin already registered under a different name"
errors.

Solution: Use explicit Python imports + re-exports to share fixtures
from sibling conftest directories.

Changes:
- Remove pytest_plugins from tests/conftest.py
- Add explicit fixture imports in tests/integration/registration/conftest.py

This pattern is the recommended approach for sharing fixtures across
sibling test directories without causing plugin registration conflicts.

@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/integration/registration/conftest.py`:
- Around line 5-6: The module docstring in
tests/integration/registration/conftest.py incorrectly mentions Kafka fixtures;
update the top-level docstring text to reflect the actual fixtures imported
(e.g., Graph, Qdrant, Vault, HTTP, Consul, PostgreSQL) and remove any reference
to Kafka so the description matches the imported fixtures used in this conftest.

Comment thread tests/integration/registration/conftest.py Outdated
…dation

- Add topic validation in _parse_event_bus_config with RuntimeHostError
- Fail-fast when event_bus_config present without valid node_id
- Wrap json.loads() calls with InfraConsulError for proper error context
- Replace ValueError with ProtocolConfigurationError in introspection
- Fix UUID parsing to skip invalid entries instead of dropping all
- Fix test operation order (update index FIRST, then store)
- Update conftest docstring to reflect actual fixtures
- Update test_raises_on_unresolved_placeholder to expect ProtocolConfigurationError
  instead of ValueError (aligns with ONEX error hierarchy)
- Add documentation explaining why inner _client null checks in closures are NOT
  redundant (type narrowing for mypy + race condition protection in async context)
…lid UUIDs

Update test_get_subscribers_for_topic_mixed_valid_invalid_uuids to match
the implementation's actual behavior: invalid UUIDs are skipped and logged,
valid ones are returned. This graceful degradation is preferred in
distributed systems where one corrupt entry shouldn't break routing.
- Add .strip() validation to reject whitespace-only topic strings in
  event_bus_config parsing (subscribe_topics and publish_topics)
- Include consul_key attribute in InfraConsulError logging for better
  debugging in get_subscribers_for_topic method

Addresses PR review feedback for improved input validation and
observability.

@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/handlers/mixins/mixin_consul_service.py (1)

395-463: Add event bus cleanup to deregistration flow.

The _deregister_service method only deregisters the service from Consul but doesn't clean up event bus config (onex/nodes/{node_id}/event_bus/) or remove the node from topic subscriber indices (onex/topics/{topic}/subscribers). The _register_service method explicitly performs both operations during registration, creating an asymmetry. The _remove_subscriber_from_topic method exists and is used elsewhere; verify whether deregistration should invoke it and any related cleanup methods to maintain consistency with the registration flow.

🧹 Nitpick comments (4)
src/omnibase_infra/runtime/service_runtime_host_process.py (1)

2332-2382: Include correlation_id in exception handler logs for traceability.

The correlation_id is created at line 2259 and used in some log statements within the try block (lines 2304, 2324-2326), but it's not included in any of the exception handler log extra dicts. This reduces observability when debugging Consul-related failures in production.

As per coding guidelines, correlation IDs should be included in all error context.

♻️ Proposed fix
         except json.JSONDecodeError as e:
             logger.warning(
                 "Failed to parse topic subscriber JSON",
                 extra={
                     "topic": topic,
                     "error": str(e),
+                    "correlation_id": str(correlation_id),
                 },
             )
             return []
         except InfraConsulError as e:
             logger.warning(
                 "Consul error querying topic subscribers",
                 extra={
                     "topic": topic,
                     "error": str(e),
                     "error_type": "InfraConsulError",
                     "consul_key": getattr(e, "consul_key", None),
+                    "correlation_id": str(correlation_id),
                 },
             )
             return []
         except InfraTimeoutError as e:
             logger.warning(
                 "Timeout querying topic subscribers",
                 extra={
                     "topic": topic,
                     "error": str(e),
                     "error_type": "InfraTimeoutError",
+                    "correlation_id": str(correlation_id),
                 },
             )
             return []
         except InfraUnavailableError as e:
             logger.warning(
                 "Service unavailable for topic subscriber query",
                 extra={
                     "topic": topic,
                     "error": str(e),
                     "error_type": "InfraUnavailableError",
+                    "correlation_id": str(correlation_id),
                 },
             )
             return []
         except Exception as e:
             # Graceful degradation - Consul unavailable is not fatal
             logger.warning(
                 "Failed to query topic subscribers from Consul",
                 extra={
                     "topic": topic,
                     "error": str(e),
                     "error_type": type(e).__name__,
+                    "correlation_id": str(correlation_id),
                 },
             )
             return []
src/omnibase_infra/handlers/mixins/mixin_consul_service.py (3)

196-198: Consider validating message_category type consistency.

The str() coercion on message_category silently converts non-string values (e.g., 123 → "123", None → "None" if no default). While the default "EVENT" handles the missing-key case, an unexpected type like int would be coerced without warning.

If strict type enforcement is desired, consider adding a type check similar to the event_type and description fields.

♻️ Suggested approach
-                            message_category=str(
-                                entry.get("message_category", "EVENT")
-                            ),
+                            message_category=entry.get("message_category")
+                            if isinstance(entry.get("message_category"), str)
+                            else "EVENT",

228-230: Same message_category coercion applies here.

The same str() coercion pattern for message_category is used in the publish_topics parsing. If the optional refactor is applied to subscribe_topics, apply it consistently here as well.


374-380: Non-atomic index update and storage sequence.

The ordering is correctly documented (update index first to compute delta from old topics, then store new config). However, if _store_node_event_bus fails after _update_topic_index succeeds, the reverse index could reference a config that wasn't persisted.

Given the PR mentions idempotent index updates, re-registration should recover from this state. Just flagging for awareness—if stronger consistency is needed later, consider wrapping both operations in a Consul transaction or implementing compensating logic.

@jonahgabriel jonahgabriel left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review

Found 3 issues requiring attention:

  1. CLAUDE.md Violation: Direct ModelInfraErrorContext() constructor usage (4 instances)
  2. Bug: Whitespace-padded topics pass validation but are stored unstripped (2 instances)
  3. Security Concern: Topic strings used in Consul KV paths without character validation

See inline comments for details.

operation="consul.kv_get_raw",
target_name="consul_handler",
correlation_id=correlation_id,
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

CLAUDE.md Violation: Direct ModelInfraErrorContext() constructor usage.

CLAUDE.md states:

MANDATORY: Use with_correlation() factory method for error context creation.

CLAUDE.md Rule

This occurs 4 times in this file (lines 125, 145, 201, 221). The file correctly uses .with_correlation() elsewhere (lines 343, 418, 473, 521), showing inconsistent application.

Suggested change
)
context = ModelInfraErrorContext.with_correlation(
correlation_id=correlation_id,
transport_type=EnumInfraTransportType.CONSUL,
operation="consul.kv_get_raw",
target_name="consul_handler",
)

Additional fixes needed at lines 145, 201, 221 - same pattern.

)
subscribe_topics.append(
ModelEventBusTopicEntry(
topic=raw_topic,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Bug: Whitespace-padded topics stored without stripping causes routing failures.

Root cause: Line 179 validates using not raw_topic.strip() to reject whitespace-only strings, but stores the unstripped raw_topic here.

Impact: A topic like " dev.topic.v1 " passes validation but gets stored with whitespace. Later lookups for "dev.topic.v1" fail because Consul KV stores the key as onex/topics/ dev.topic.v1 /subscribers.

Suggested change
topic=raw_topic,
topic=raw_topic.strip(),

Same fix needed at line 224 for publish_topics.

concurrency notes. Accepted MVP limitation.
"""
# NOTE: Non-atomic read-modify-write. See _update_topic_index for details.
key = f"onex/topics/{topic}/subscribers"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Security Concern: Topic strings used in Consul KV paths without character validation.

Topics are interpolated directly into KV paths:

key = f"onex/topics/{topic}/subscribers"

A topic containing slashes (e.g., foo/bar) or other special characters could:

  • Create unexpected key hierarchy
  • Potentially bypass ACL prefix matching rules
  • Cause key parsing issues

Note: The codebase has robust topic validation in event_bus_kafka.py (lines 1420-1479) using regex ^[a-zA-Z0-9._-]+$, but this is NOT called in the Consul topic index flow.

Fix src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py: Add topic format validation before using in KV paths. Either reuse the Kafka validation pattern (regex ^[a-zA-Z0-9._-]+$) or add a @field_validator to ModelEventBusTopicEntry.topic to enforce this at the model level.

…use error context factory

- Strip whitespace from topic strings before storage to prevent routing mismatches
- Add topic format validation (alphanumeric, periods, underscores, hyphens only)
  to prevent Consul KV path traversal via slashes or special characters
- Replace direct ModelInfraErrorContext() calls with .with_correlation() factory
  method per CLAUDE.md requirements (4 instances)
…or handling

- Add _validate_topic_entry method to mixin_consul_service.py for
  consolidated topic validation (format, whitespace, message_category)
- Replace plain ValueError with ProtocolConfigurationError in
  mixin_node_introspection.py for structured error reporting
- Add correlation_id to all exception handler logs for traceability
- Add fail-fast check when event_bus_config present without valid node_id
- Update conftest.py docstring with comprehensive fixture documentation
- Document operation order consistency in test_introspection_event_bus_e2e.py
…r messages

The topic validation was checking format (alphanumeric, dots, hyphens,
underscores) before checking for unresolved placeholders like {env}.
Since curly braces fail the format regex, users got a confusing "Invalid
topic suffix format" error instead of the more helpful "Unresolved
placeholder in topic" message.

Changes:
- Move placeholder detection BEFORE format validation in resolve_topic()
- Raise ValueError from _extract_event_bus_config for placeholders
- Wrap ValueError in ProtocolConfigurationError in get_introspection_data
  with "Event bus extraction failed" message for consistent error handling

This restores the expected test behavior where placeholder errors are
caught early with a specific, actionable error message.
…ent-bus-topic-storage-to-registry-for-dynamic-topic
…lper

After merging main (OMN-1602), RuntimeHostProcess now requires
service_name and node_name in config. Update test fixtures to use
the make_runtime_config() helper from tests/conftest.py.
…dule

Extract duplicate topic validation regex to `constants_topic_patterns.py`:
- Create new `TOPIC_NAME_PATTERN` constant in dependency-free module
- Remove inline `import re` from `mixin_node_introspection.py`
- Update all topic index mixins to use shared constant
- Prevents circular imports and ensures consistent validation

This addresses PR #199 code review feedback to consolidate duplicate
regex patterns and improve code quality.

@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

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/mixins/mixin_node_introspection.py (1)

1680-1686: Prevent UnboundLocalError in exception logging.

If get_introspection_data() raises before final_correlation_id is assigned, Line 1684 will throw UnboundLocalError, masking the original error. Initialize the correlation id before the try.

🐛 Suggested fix
-        async with self.track_operation("publish_introspection"):
+        final_correlation_id = correlation_id or uuid4()
+        async with self.track_operation("publish_introspection"):
             try:
                 # Get introspection data
                 event = await self.get_introspection_data()
 
                 # Create publish event with updated reason and correlation_id
                 # Use model_copy for clean field updates (Pydantic v2)
-                final_correlation_id = correlation_id or uuid4()
                 publish_event = event.model_copy(
                     update={
                         "reason": reason_enum,
                         "correlation_id": final_correlation_id,
                     }
                 )
🤖 Fix all issues with AI agents
In `@src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py`:
- Around line 108-139: In _validate_topic_format, guard against non-string topic
inputs so TOPIC_NAME_PATTERN.match(topic) cannot raise a raw TypeError: add an
explicit type check (isinstance(topic, str)) at the top of the method and, if it
fails, construct the same ModelInfraErrorContext (use correlation_id,
EnumInfraTransportType.CONSUL, operation="validate_topic_format",
target_name="consul_handler") and raise ProtocolConfigurationError with
parameter="topic" and value=topic and a clear message that topic must be a
string; otherwise proceed to use TOPIC_NAME_PATTERN.match(topic) as before so
all failures surface as OnexError subclasses.

In `@src/omnibase_infra/mixins/mixin_node_introspection.py`:
- Around line 1044-1057: The helper resolve_topic currently raises a plain
ValueError on unresolved placeholders; change that to raise
ProtocolConfigurationError (an OnexError subclass) with a clear message and
context so callers don't need to catch/translate ValueError. Locate
resolve_topic in mixin_node_introspection.py and replace the ValueError raise
with ProtocolConfigurationError(...) including the offending suffix in the
message (and any relevant metadata if the ProtocolConfigurationError supports
it), ensuring imports/reference to ProtocolConfigurationError are used instead
of ValueError.

Comment on lines +108 to +139
def _validate_topic_format(self, topic: str, correlation_id: UUID) -> None:
"""Validate topic format for safe use in Consul KV paths.

Topics are interpolated into Consul KV paths like `onex/topics/{topic}/subscribers`.
Invalid characters (especially slashes) could create unexpected key hierarchies
or bypass ACL prefix matching.

Args:
topic: The topic string to validate.
correlation_id: Correlation ID for error context.

Raises:
ProtocolConfigurationError: If topic contains invalid characters.
Valid characters: alphanumeric (a-zA-Z0-9), periods (.), underscores (_),
and hyphens (-).
"""
if not TOPIC_NAME_PATTERN.match(topic):
context = ModelInfraErrorContext.with_correlation(
correlation_id=correlation_id,
transport_type=EnumInfraTransportType.CONSUL,
operation="validate_topic_format",
target_name="consul_handler",
)
raise ProtocolConfigurationError(
f"Topic name '{topic}' contains invalid characters. "
"Only alphanumeric characters, periods (.), underscores (_), "
"and hyphens (-) are allowed. Slashes and other special "
"characters are prohibited to prevent Consul KV path traversal.",
context=context,
parameter="topic",
value=topic,
)

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

Guard non-string topics to avoid raw TypeError.

If corrupted KV data yields a non-string topic, TOPIC_NAME_PATTERN.match() will raise TypeError and bypass the OnexError contract. A small type guard keeps failures typed and traceable. As per coding guidelines, only OnexError subclasses should surface from these paths.

🐛 Proposed fix
     def _validate_topic_format(self, topic: str, correlation_id: UUID) -> None:
         """Validate topic format for safe use in Consul KV paths.
@@
-        if not TOPIC_NAME_PATTERN.match(topic):
+        if not isinstance(topic, str):
+            context = ModelInfraErrorContext.with_correlation(
+                correlation_id=correlation_id,
+                transport_type=EnumInfraTransportType.CONSUL,
+                operation="validate_topic_format",
+                target_name="consul_handler",
+            )
+            raise ProtocolConfigurationError(
+                f"Topic must be a string, got {type(topic).__name__}",
+                context=context,
+                parameter="topic",
+                value=str(topic),
+            )
+        if not TOPIC_NAME_PATTERN.match(topic):
             context = ModelInfraErrorContext.with_correlation(
                 correlation_id=correlation_id,
                 transport_type=EnumInfraTransportType.CONSUL,
                 operation="validate_topic_format",
                 target_name="consul_handler",
             )
📝 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
def _validate_topic_format(self, topic: str, correlation_id: UUID) -> None:
"""Validate topic format for safe use in Consul KV paths.
Topics are interpolated into Consul KV paths like `onex/topics/{topic}/subscribers`.
Invalid characters (especially slashes) could create unexpected key hierarchies
or bypass ACL prefix matching.
Args:
topic: The topic string to validate.
correlation_id: Correlation ID for error context.
Raises:
ProtocolConfigurationError: If topic contains invalid characters.
Valid characters: alphanumeric (a-zA-Z0-9), periods (.), underscores (_),
and hyphens (-).
"""
if not TOPIC_NAME_PATTERN.match(topic):
context = ModelInfraErrorContext.with_correlation(
correlation_id=correlation_id,
transport_type=EnumInfraTransportType.CONSUL,
operation="validate_topic_format",
target_name="consul_handler",
)
raise ProtocolConfigurationError(
f"Topic name '{topic}' contains invalid characters. "
"Only alphanumeric characters, periods (.), underscores (_), "
"and hyphens (-) are allowed. Slashes and other special "
"characters are prohibited to prevent Consul KV path traversal.",
context=context,
parameter="topic",
value=topic,
)
def _validate_topic_format(self, topic: str, correlation_id: UUID) -> None:
"""Validate topic format for safe use in Consul KV paths.
Topics are interpolated into Consul KV paths like `onex/topics/{topic}/subscribers`.
Invalid characters (especially slashes) could create unexpected key hierarchies
or bypass ACL prefix matching.
Args:
topic: The topic string to validate.
correlation_id: Correlation ID for error context.
Raises:
ProtocolConfigurationError: If topic contains invalid characters.
Valid characters: alphanumeric (a-zA-Z0-9), periods (.), underscores (_),
and hyphens (-).
"""
try:
if not TOPIC_NAME_PATTERN.match(topic):
context = ModelInfraErrorContext.with_correlation(
correlation_id=correlation_id,
transport_type=EnumInfraTransportType.CONSUL,
operation="validate_topic_format",
target_name="consul_handler",
)
raise ProtocolConfigurationError(
f"Topic name '{topic}' contains invalid characters. "
"Only alphanumeric characters, periods (.), underscores (_), "
"and hyphens (-) are allowed. Slashes and other special "
"characters are prohibited to prevent Consul KV path traversal.",
context=context,
parameter="topic",
value=topic,
)
except TypeError as e:
context = ModelInfraErrorContext.with_correlation(
correlation_id=correlation_id,
transport_type=EnumInfraTransportType.CONSUL,
operation="validate_topic_format",
target_name="consul_handler",
)
raise ProtocolConfigurationError(
f"Topic must be a string, got {type(topic).__name__}",
context=context,
parameter="topic",
value=str(topic),
) from e
🤖 Prompt for AI Agents
In `@src/omnibase_infra/handlers/mixins/mixin_consul_topic_index.py` around lines
108 - 139, In _validate_topic_format, guard against non-string topic inputs so
TOPIC_NAME_PATTERN.match(topic) cannot raise a raw TypeError: add an explicit
type check (isinstance(topic, str)) at the top of the method and, if it fails,
construct the same ModelInfraErrorContext (use correlation_id,
EnumInfraTransportType.CONSUL, operation="validate_topic_format",
target_name="consul_handler") and raise ProtocolConfigurationError with
parameter="topic" and value=topic and a clear message that topic must be a
string; otherwise proceed to use TOPIC_NAME_PATTERN.match(topic) as before so
all failures surface as OnexError subclasses.

Comment on lines +1044 to +1057
def resolve_topic(suffix: str) -> str:
"""Resolve topic suffix to full topic with env prefix."""
# Full topic format: {env}.{suffix}
# Strip whitespace from suffix to handle YAML formatting artifacts
suffix = suffix.strip()

# Fail-fast: check for unresolved placeholders BEFORE format validation
# This provides more helpful error messages when placeholders aren't resolved
if "{" in suffix or "}" in suffix:
raise ValueError(
f"Unresolved placeholder in topic: '{suffix}'. "
"Ensure all placeholders like {env} or {namespace} are resolved "
"before topic validation."
)

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 ProtocolConfigurationError for unresolved placeholders.

Line 1052 raises a plain ValueError, which can leak outside this helper and violates the “OnexError-only” rule. Raise ProtocolConfigurationError with context instead (and the caller no longer needs to wrap ValueError). As per coding guidelines, only OnexError subclasses should be raised.

♻️ Suggested fix
-            if "{" in suffix or "}" in suffix:
-                raise ValueError(
-                    f"Unresolved placeholder in topic: '{suffix}'. "
-                    "Ensure all placeholders like {env} or {namespace} are resolved "
-                    "before topic validation."
-                )
+            if "{" in suffix or "}" in suffix:
+                context = ModelInfraErrorContext.with_correlation(
+                    transport_type=EnumInfraTransportType.RUNTIME,
+                    operation="_extract_event_bus_config",
+                    target_name=suffix,
+                )
+                raise ProtocolConfigurationError(
+                    f"Unresolved placeholder in topic: '{suffix}'. "
+                    "Ensure all placeholders like {env} or {namespace} are resolved "
+                    "before topic validation.",
+                    context=context,
+                    parameter="topic_suffix",
+                )
📝 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
def resolve_topic(suffix: str) -> str:
"""Resolve topic suffix to full topic with env prefix."""
# Full topic format: {env}.{suffix}
# Strip whitespace from suffix to handle YAML formatting artifacts
suffix = suffix.strip()
# Fail-fast: check for unresolved placeholders BEFORE format validation
# This provides more helpful error messages when placeholders aren't resolved
if "{" in suffix or "}" in suffix:
raise ValueError(
f"Unresolved placeholder in topic: '{suffix}'. "
"Ensure all placeholders like {env} or {namespace} are resolved "
"before topic validation."
)
def resolve_topic(suffix: str) -> str:
"""Resolve topic suffix to full topic with env prefix."""
# Full topic format: {env}.{suffix}
# Strip whitespace from suffix to handle YAML formatting artifacts
suffix = suffix.strip()
# Fail-fast: check for unresolved placeholders BEFORE format validation
# This provides more helpful error messages when placeholders aren't resolved
if "{" in suffix or "}" in suffix:
context = ModelInfraErrorContext.with_correlation(
transport_type=EnumInfraTransportType.RUNTIME,
operation="_extract_event_bus_config",
target_name=suffix,
)
raise ProtocolConfigurationError(
f"Unresolved placeholder in topic: '{suffix}'. "
"Ensure all placeholders like {env} or {namespace} are resolved "
"before topic validation.",
context=context,
parameter="topic_suffix",
)
🤖 Prompt for AI Agents
In `@src/omnibase_infra/mixins/mixin_node_introspection.py` around lines 1044 -
1057, The helper resolve_topic currently raises a plain ValueError on unresolved
placeholders; change that to raise ProtocolConfigurationError (an OnexError
subclass) with a clear message and context so callers don't need to
catch/translate ValueError. Locate resolve_topic in mixin_node_introspection.py
and replace the ValueError raise with ProtocolConfigurationError(...) including
the offending suffix in the message (and any relevant metadata if the
ProtocolConfigurationError supports it), ensuring imports/reference to
ProtocolConfigurationError are used instead of ValueError.

@jonahgabriel
jonahgabriel merged commit 0f07876 into main Jan 27, 2026
19 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-1613-add-event-bus-topic-storage-to-registry-for-dynamic-topic 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