omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_handler_source_mode.py +16 -2
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
- omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
- omnibase_infra/event_bus/event_bus_kafka.py +105 -47
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
- omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/handlers/handler_consul.py +2 -0
- omnibase_infra/handlers/mixins/__init__.py +5 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/mixin_node_introspection.py +189 -19
- omnibase_infra/models/__init__.py +8 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/discovery/model_introspection_config.py +25 -17
- omnibase_infra/models/dispatch/__init__.py +8 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
- omnibase_infra/models/registration/__init__.py +9 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
- omnibase_infra/models/runtime/__init__.py +9 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
- omnibase_infra/nodes/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
- omnibase_infra/nodes/reducers/models/__init__.py +7 -2
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
- omnibase_infra/protocols/__init__.py +3 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/runtime/__init__.py +60 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_source_resolver.py +43 -2
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/models/__init__.py +13 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/service_kernel.py +76 -6
- omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
- omnibase_infra/runtime/service_runtime_host_process.py +770 -20
- omnibase_infra/runtime/transition_notification_publisher.py +3 -2
- omnibase_infra/runtime/util_wiring.py +206 -62
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
- omnibase_infra/services/session/config_consumer.py +25 -8
- omnibase_infra/services/session/config_store.py +2 -2
- omnibase_infra/services/session/consumer.py +1 -1
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/typed_dict/__init__.py +9 -1
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/utils/__init__.py +9 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/validation/infra_validators.py +18 -1
- omnibase_infra/validation/validation_exemptions.yaml +192 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -49,7 +49,7 @@ Example Usage:
|
|
|
49
49
|
# Initialize publisher with event bus
|
|
50
50
|
publisher = TransitionNotificationPublisher(
|
|
51
51
|
event_bus=kafka_event_bus,
|
|
52
|
-
topic=
|
|
52
|
+
topic=SUFFIX_FSM_STATE_TRANSITIONS,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
# Publish single notification
|
|
@@ -110,6 +110,7 @@ from omnibase_infra.models.resilience import ModelCircuitBreakerConfig
|
|
|
110
110
|
from omnibase_infra.runtime.models.model_transition_notification_publisher_metrics import (
|
|
111
111
|
ModelTransitionNotificationPublisherMetrics,
|
|
112
112
|
)
|
|
113
|
+
from omnibase_infra.topics import SUFFIX_FSM_STATE_TRANSITIONS
|
|
113
114
|
from omnibase_infra.utils.util_error_sanitization import sanitize_error_string
|
|
114
115
|
|
|
115
116
|
if TYPE_CHECKING:
|
|
@@ -754,7 +755,7 @@ def _verify_protocol_compliance() -> None: # pragma: no cover
|
|
|
754
755
|
publisher: ProtocolTransitionNotificationPublisher = (
|
|
755
756
|
TransitionNotificationPublisher(
|
|
756
757
|
event_bus=bus,
|
|
757
|
-
topic=
|
|
758
|
+
topic=SUFFIX_FSM_STATE_TRANSITIONS,
|
|
758
759
|
)
|
|
759
760
|
)
|
|
760
761
|
# Use the variable to silence unused warnings
|
|
@@ -7,11 +7,20 @@ with the RegistryProtocolBinding and RegistryEventBusBinding. It serves as
|
|
|
7
7
|
the bridge between handler implementations and the registry system.
|
|
8
8
|
|
|
9
9
|
The wiring module is responsible for:
|
|
10
|
-
- Registering default handlers
|
|
10
|
+
- Registering default handlers from contract.yaml files
|
|
11
11
|
- Registering handlers based on contract configuration
|
|
12
12
|
- Validating that requested handler types are known and supported
|
|
13
13
|
- Providing a summary of registered handlers for debugging
|
|
14
14
|
|
|
15
|
+
Contract-Driven Handler Loading:
|
|
16
|
+
Handler classes are discovered and loaded from contract.yaml files located
|
|
17
|
+
in nodes/handlers/<handler_type>/contract.yaml. Each contract specifies:
|
|
18
|
+
- handler.module: The Python module path
|
|
19
|
+
- handler.name: The class name to load
|
|
20
|
+
|
|
21
|
+
This replaces the old hardcoded _KNOWN_HANDLERS dict with dynamic,
|
|
22
|
+
contract-based discovery.
|
|
23
|
+
|
|
15
24
|
Event Bus Support:
|
|
16
25
|
This module registers EventBusInmemory as the default event bus. For production
|
|
17
26
|
deployments requiring EventBusKafka, the event bus is selected at kernel bootstrap
|
|
@@ -22,10 +31,13 @@ Event Bus Support:
|
|
|
22
31
|
See kernel.py for event bus selection logic during runtime bootstrap.
|
|
23
32
|
|
|
24
33
|
Design Principles:
|
|
34
|
+
- Contract-driven: Handler configurations live in contract.yaml, not Python code
|
|
25
35
|
- Explicit wiring: All handler registrations are explicit, not auto-discovered
|
|
26
|
-
- Contract-driven: Supports wiring from contract configuration dicts
|
|
27
36
|
- Validation: Unknown handler types raise clear errors
|
|
37
|
+
- Fail-fast: Missing contracts raise FileNotFoundError immediately
|
|
28
38
|
- Idempotent: Re-wiring the same handler is safe (overwrites previous)
|
|
39
|
+
- Security: Namespace allowlisting is recommended for production deployments
|
|
40
|
+
(see docs/patterns/handler_plugin_loader.md#optional-security-controls)
|
|
29
41
|
|
|
30
42
|
Adding New Handlers:
|
|
31
43
|
To add a new handler to the system, follow these steps:
|
|
@@ -48,16 +60,24 @@ Adding New Handlers:
|
|
|
48
60
|
return {"success": True, "data": ...}
|
|
49
61
|
```
|
|
50
62
|
|
|
51
|
-
2.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
2. Create a contract.yaml in nodes/handlers/<type>/contract.yaml:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
name: "handler_custom"
|
|
67
|
+
node_type: "EFFECT_GENERIC"
|
|
68
|
+
description: "Custom protocol handler"
|
|
69
|
+
handler_routing:
|
|
70
|
+
routing_strategy: "operation_match"
|
|
71
|
+
handlers:
|
|
72
|
+
- handler_type: "custom"
|
|
73
|
+
handler:
|
|
74
|
+
name: "MyCustomHandler"
|
|
75
|
+
module: "mypackage.handlers.handler_custom"
|
|
76
|
+
```
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
HANDLER_TYPE_CUSTOM: (MyCustomHandler, "Custom protocol handler"),
|
|
78
|
+
3. Add the contract path to _HANDLER_CONTRACT_PATHS in this module.
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
wire_custom_handler():
|
|
80
|
+
4. For runtime registration without contracts, use wire_custom_handler():
|
|
61
81
|
|
|
62
82
|
```python
|
|
63
83
|
from omnibase_infra.runtime.util_wiring import wire_custom_handler
|
|
@@ -94,7 +114,7 @@ Example Usage:
|
|
|
94
114
|
wire_handlers_from_contract,
|
|
95
115
|
)
|
|
96
116
|
|
|
97
|
-
# Wire all default handlers
|
|
117
|
+
# Wire all default handlers from contracts
|
|
98
118
|
summary = wire_default_handlers()
|
|
99
119
|
print(f"Registered handlers: {summary['handlers']}")
|
|
100
120
|
print(f"Registered event buses: {summary['event_buses']}")
|
|
@@ -113,19 +133,16 @@ Example Usage:
|
|
|
113
133
|
|
|
114
134
|
from __future__ import annotations
|
|
115
135
|
|
|
136
|
+
import importlib
|
|
116
137
|
import logging
|
|
138
|
+
from pathlib import Path
|
|
117
139
|
from typing import TYPE_CHECKING
|
|
118
140
|
|
|
141
|
+
import yaml
|
|
142
|
+
|
|
119
143
|
from omnibase_core.types import JsonType
|
|
120
144
|
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
121
145
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
122
|
-
from omnibase_infra.handlers.handler_consul import HandlerConsul
|
|
123
|
-
from omnibase_infra.handlers.handler_db import HandlerDb
|
|
124
|
-
from omnibase_infra.handlers.handler_graph import HandlerGraph
|
|
125
|
-
from omnibase_infra.handlers.handler_http import HandlerHttpRest
|
|
126
|
-
from omnibase_infra.handlers.handler_intent import HandlerIntent
|
|
127
|
-
from omnibase_infra.handlers.handler_mcp import HandlerMCP
|
|
128
|
-
from omnibase_infra.handlers.handler_vault import HandlerVault
|
|
129
146
|
from omnibase_infra.runtime.handler_registry import (
|
|
130
147
|
EVENT_BUS_INMEMORY,
|
|
131
148
|
HANDLER_TYPE_CONSUL,
|
|
@@ -147,39 +164,20 @@ if TYPE_CHECKING:
|
|
|
147
164
|
|
|
148
165
|
logger = logging.getLogger(__name__)
|
|
149
166
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
# To add a new handler:
|
|
166
|
-
# 1. Define HANDLER_TYPE_XXX constant in handler_registry.py
|
|
167
|
-
# 2. Import the handler class at the top of this module
|
|
168
|
-
# 3. Add entry below: HANDLER_TYPE_XXX: (XxxHandler, "Description"),
|
|
169
|
-
#
|
|
170
|
-
# NOTE: HandlerHttpRest and HandlerDb use legacy execute(envelope: dict) signature.
|
|
171
|
-
# They will be migrated to ProtocolHandler.execute(request, operation_config) in future.
|
|
172
|
-
# Type ignore comments suppress MyPy errors during MVP phase.
|
|
173
|
-
_KNOWN_HANDLERS: dict[str, tuple[type[ProtocolContainerAware], str]] = {
|
|
174
|
-
# NOTE: Handlers implement ProtocolHandler structurally but concrete types differ from protocol.
|
|
175
|
-
HANDLER_TYPE_CONSUL: (HandlerConsul, "HashiCorp Consul service discovery handler"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
176
|
-
HANDLER_TYPE_DATABASE: (HandlerDb, "PostgreSQL database handler"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
177
|
-
HANDLER_TYPE_GRAPH: (HandlerGraph, "Graph database (Memgraph/Neo4j) handler"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
178
|
-
HANDLER_TYPE_HTTP: (HandlerHttpRest, "HTTP REST protocol handler"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
179
|
-
# DEMO: Temporary registration - remove when contract-driven (OMN-1515)
|
|
180
|
-
HANDLER_TYPE_INTENT: (HandlerIntent, "Intent storage and query handler for demo"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
181
|
-
HANDLER_TYPE_MCP: (HandlerMCP, "Model Context Protocol handler for AI agents"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
182
|
-
HANDLER_TYPE_VAULT: (HandlerVault, "HashiCorp Vault secret management handler"), # type: ignore[dict-item] # NOTE: structural subtyping
|
|
167
|
+
# Handler contract directory path.
|
|
168
|
+
# Handler configurations are loaded from contract.yaml files in this directory.
|
|
169
|
+
_HANDLERS_BASE = Path(__file__).parent.parent / "nodes" / "handlers"
|
|
170
|
+
|
|
171
|
+
# Mapping of handler types to their contract paths.
|
|
172
|
+
# Each entry maps a handler type constant to the path of its contract.yaml file.
|
|
173
|
+
_HANDLER_CONTRACT_PATHS: dict[str, Path] = {
|
|
174
|
+
HANDLER_TYPE_CONSUL: _HANDLERS_BASE / "consul" / "contract.yaml",
|
|
175
|
+
HANDLER_TYPE_DATABASE: _HANDLERS_BASE / "db" / "contract.yaml",
|
|
176
|
+
HANDLER_TYPE_GRAPH: _HANDLERS_BASE / "graph" / "contract.yaml",
|
|
177
|
+
HANDLER_TYPE_HTTP: _HANDLERS_BASE / "http" / "contract.yaml",
|
|
178
|
+
HANDLER_TYPE_INTENT: _HANDLERS_BASE / "intent" / "contract.yaml",
|
|
179
|
+
HANDLER_TYPE_MCP: _HANDLERS_BASE / "mcp" / "contract.yaml",
|
|
180
|
+
HANDLER_TYPE_VAULT: _HANDLERS_BASE / "vault" / "contract.yaml",
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
# Known event bus kinds that can be wired via this module.
|
|
@@ -194,6 +192,142 @@ _KNOWN_EVENT_BUSES: dict[str, tuple[type[ProtocolEventBus], str]] = {
|
|
|
194
192
|
}
|
|
195
193
|
|
|
196
194
|
|
|
195
|
+
def _load_handler_from_contract(
|
|
196
|
+
handler_type: str, contract_path: Path
|
|
197
|
+
) -> tuple[type[ProtocolContainerAware], str]:
|
|
198
|
+
"""Load handler class from a contract.yaml file.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
handler_type: The handler type identifier (e.g., "consul", "db").
|
|
202
|
+
contract_path: Path to the contract.yaml file.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (handler_class, description).
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
FileNotFoundError: If contract file does not exist.
|
|
209
|
+
ProtocolConfigurationError: If contract is malformed or handler cannot be loaded.
|
|
210
|
+
|
|
211
|
+
Security Note:
|
|
212
|
+
This function uses ``importlib.import_module()`` to dynamically load handler
|
|
213
|
+
modules specified in contracts. This means contract files are effectively
|
|
214
|
+
executable code - a compromised contract pointing to a malicious module
|
|
215
|
+
will execute that module's code during import.
|
|
216
|
+
|
|
217
|
+
**Production Security Recommendations:**
|
|
218
|
+
|
|
219
|
+
1. **Namespace Allowlisting**: For dynamic handler discovery scenarios,
|
|
220
|
+
use ``HandlerPluginLoader`` with the ``allowed_namespaces`` parameter
|
|
221
|
+
to restrict which module namespaces can be loaded::
|
|
222
|
+
|
|
223
|
+
from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
|
|
224
|
+
|
|
225
|
+
loader = HandlerPluginLoader(
|
|
226
|
+
allowed_namespaces=["omnibase_infra.", "omnibase_core.", "myapp.handlers."]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
This prevents loading handlers from untrusted namespaces even if a
|
|
230
|
+
contract is compromised.
|
|
231
|
+
|
|
232
|
+
2. **Write Protection**: Contract directories should be read-only at runtime.
|
|
233
|
+
Mount contract directories as read-only volumes in containerized deployments.
|
|
234
|
+
|
|
235
|
+
3. **Source Validation**: Contracts in ``_HANDLER_CONTRACT_PATHS`` come from
|
|
236
|
+
the omnibase_infra package. Ensure these are from trusted, version-controlled
|
|
237
|
+
sources with code review.
|
|
238
|
+
|
|
239
|
+
See Also:
|
|
240
|
+
- ``docs/patterns/handler_plugin_loader.md#optional-security-controls``
|
|
241
|
+
- ``docs/patterns/security_patterns.md``
|
|
242
|
+
- ``docs/decisions/adr-handler-plugin-loader-security.md``
|
|
243
|
+
"""
|
|
244
|
+
if not contract_path.exists():
|
|
245
|
+
raise FileNotFoundError(
|
|
246
|
+
f"Handler contract not found: {contract_path}. "
|
|
247
|
+
f"All handlers must have contract.yaml files."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
with contract_path.open("r") as f:
|
|
251
|
+
contract = yaml.safe_load(f)
|
|
252
|
+
|
|
253
|
+
if contract is None:
|
|
254
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
255
|
+
operation="load_handler_contract",
|
|
256
|
+
target_name=str(contract_path),
|
|
257
|
+
)
|
|
258
|
+
raise ProtocolConfigurationError(
|
|
259
|
+
f"Empty contract file: {contract_path}",
|
|
260
|
+
context=context,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
handler_routing = contract.get("handler_routing", {})
|
|
264
|
+
handlers = handler_routing.get("handlers", [])
|
|
265
|
+
|
|
266
|
+
if not handlers:
|
|
267
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
268
|
+
operation="load_handler_contract",
|
|
269
|
+
target_name=str(contract_path),
|
|
270
|
+
)
|
|
271
|
+
raise ProtocolConfigurationError(
|
|
272
|
+
f"No handlers defined in contract: {contract_path}",
|
|
273
|
+
context=context,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
handler_def = handlers[0]
|
|
277
|
+
handler_info = handler_def.get("handler", {})
|
|
278
|
+
handler_module = handler_info.get("module")
|
|
279
|
+
handler_class_name = handler_info.get("name")
|
|
280
|
+
|
|
281
|
+
if not handler_module or not handler_class_name:
|
|
282
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
283
|
+
operation="load_handler_contract",
|
|
284
|
+
target_name=str(contract_path),
|
|
285
|
+
)
|
|
286
|
+
raise ProtocolConfigurationError(
|
|
287
|
+
f"Missing handler module or name in contract: {contract_path}. "
|
|
288
|
+
f"Expected handler.module and handler.name fields.",
|
|
289
|
+
context=context,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
module = importlib.import_module(handler_module)
|
|
294
|
+
except ImportError as e:
|
|
295
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
296
|
+
operation="import_handler_module",
|
|
297
|
+
target_name=handler_module,
|
|
298
|
+
)
|
|
299
|
+
raise ProtocolConfigurationError(
|
|
300
|
+
f"Failed to import handler module '{handler_module}': {e}",
|
|
301
|
+
context=context,
|
|
302
|
+
) from e
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
handler_class = getattr(module, handler_class_name)
|
|
306
|
+
except AttributeError as e:
|
|
307
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
308
|
+
operation="load_handler_class",
|
|
309
|
+
target_name=f"{handler_module}.{handler_class_name}",
|
|
310
|
+
)
|
|
311
|
+
raise ProtocolConfigurationError(
|
|
312
|
+
f"Handler class '{handler_class_name}' not found in module '{handler_module}'",
|
|
313
|
+
context=context,
|
|
314
|
+
) from e
|
|
315
|
+
|
|
316
|
+
description = contract.get("description", f"{handler_type} handler")
|
|
317
|
+
|
|
318
|
+
logger.debug(
|
|
319
|
+
"Loaded handler from contract",
|
|
320
|
+
extra={
|
|
321
|
+
"handler_type": handler_type,
|
|
322
|
+
"handler_class": handler_class_name,
|
|
323
|
+
"handler_module": handler_module,
|
|
324
|
+
"contract_path": str(contract_path),
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return handler_class, description
|
|
329
|
+
|
|
330
|
+
|
|
197
331
|
def wire_default_handlers() -> dict[str, list[str]]:
|
|
198
332
|
"""Register all default handlers and event buses with singleton registries.
|
|
199
333
|
|
|
@@ -240,17 +374,21 @@ def wire_default_handlers() -> dict[str, list[str]]:
|
|
|
240
374
|
handler_registry = get_handler_registry()
|
|
241
375
|
event_bus_registry = get_event_bus_registry()
|
|
242
376
|
|
|
243
|
-
# Register all
|
|
244
|
-
for handler_type,
|
|
377
|
+
# Register all handlers from contracts
|
|
378
|
+
for handler_type, contract_path in _HANDLER_CONTRACT_PATHS.items():
|
|
379
|
+
handler_cls, description = _load_handler_from_contract(
|
|
380
|
+
handler_type, contract_path
|
|
381
|
+
)
|
|
245
382
|
# NOTE: Handlers implement ProtocolHandler structurally but don't inherit from it.
|
|
246
383
|
# Mypy cannot verify structural subtyping for registration argument.
|
|
247
384
|
handler_registry.register(handler_type, handler_cls) # type: ignore[arg-type] # NOTE: structural subtyping
|
|
248
385
|
logger.debug(
|
|
249
|
-
"Registered handler",
|
|
386
|
+
"Registered handler from contract",
|
|
250
387
|
extra={
|
|
251
388
|
"handler_type": handler_type,
|
|
252
389
|
"handler_class": handler_cls.__name__,
|
|
253
390
|
"description": description,
|
|
391
|
+
"contract_path": str(contract_path),
|
|
254
392
|
},
|
|
255
393
|
)
|
|
256
394
|
|
|
@@ -379,28 +517,32 @@ def wire_handlers_from_contract(
|
|
|
379
517
|
)
|
|
380
518
|
continue
|
|
381
519
|
|
|
382
|
-
# Validate handler type is known
|
|
383
|
-
if handler_type not in
|
|
384
|
-
known_types = sorted(
|
|
520
|
+
# Validate handler type is known (has a contract)
|
|
521
|
+
if handler_type not in _HANDLER_CONTRACT_PATHS:
|
|
522
|
+
known_types = sorted(_HANDLER_CONTRACT_PATHS.keys())
|
|
385
523
|
raise ProtocolConfigurationError(
|
|
386
524
|
f"Unknown handler type: {handler_type!r}. "
|
|
387
525
|
f"Known types: {known_types}",
|
|
388
526
|
context=_make_error_context("validate_handler_type", handler_type),
|
|
389
527
|
)
|
|
390
528
|
|
|
391
|
-
#
|
|
392
|
-
|
|
529
|
+
# Load and register the handler from contract
|
|
530
|
+
contract_path = _HANDLER_CONTRACT_PATHS[handler_type]
|
|
531
|
+
handler_cls, description = _load_handler_from_contract(
|
|
532
|
+
handler_type, contract_path
|
|
533
|
+
)
|
|
393
534
|
# NOTE: Handlers implement ProtocolHandler structurally but don't inherit from it.
|
|
394
535
|
# Mypy cannot verify structural subtyping for registration argument.
|
|
395
536
|
handler_registry.register(handler_type, handler_cls) # type: ignore[arg-type] # NOTE: structural subtyping
|
|
396
537
|
registered_handlers.append(handler_type)
|
|
397
538
|
|
|
398
539
|
logger.debug(
|
|
399
|
-
"Registered handler from contract",
|
|
540
|
+
"Registered handler from contract config",
|
|
400
541
|
extra={
|
|
401
542
|
"handler_type": handler_type,
|
|
402
543
|
"handler_class": handler_cls.__name__,
|
|
403
544
|
"description": description,
|
|
545
|
+
"contract_path": str(contract_path),
|
|
404
546
|
},
|
|
405
547
|
)
|
|
406
548
|
|
|
@@ -471,14 +613,16 @@ def wire_handlers_from_contract(
|
|
|
471
613
|
def get_known_handler_types() -> list[str]:
|
|
472
614
|
"""Get list of known handler types that can be wired.
|
|
473
615
|
|
|
616
|
+
Handler types are discovered from contract.yaml files in nodes/handlers/.
|
|
617
|
+
|
|
474
618
|
Returns:
|
|
475
619
|
Sorted list of handler type strings.
|
|
476
620
|
|
|
477
621
|
Example:
|
|
478
622
|
>>> get_known_handler_types()
|
|
479
|
-
['db', 'http']
|
|
623
|
+
['consul', 'db', 'graph', 'http', 'intent', 'mcp', 'vault']
|
|
480
624
|
"""
|
|
481
|
-
return sorted(
|
|
625
|
+
return sorted(_HANDLER_CONTRACT_PATHS.keys())
|
|
482
626
|
|
|
483
627
|
|
|
484
628
|
def get_known_event_bus_kinds() -> list[str]:
|
|
@@ -8,7 +8,7 @@ the MCP tool registry in real-time. It supports:
|
|
|
8
8
|
- Deregistration: Removed orchestrators are removed from tool registry
|
|
9
9
|
- Idempotency: Duplicate/out-of-order events are handled correctly
|
|
10
10
|
|
|
11
|
-
Event Topic: node
|
|
11
|
+
Event Topic: Uses SUFFIX_NODE_REGISTRATION (onex.evt.platform.node-registration.v1)
|
|
12
12
|
Event Types:
|
|
13
13
|
- registered: New node registered → upsert tool
|
|
14
14
|
- updated: Node updated → upsert tool
|
|
@@ -26,9 +26,11 @@ from uuid import uuid4
|
|
|
26
26
|
|
|
27
27
|
from omnibase_core.container import ModelONEXContainer
|
|
28
28
|
from omnibase_core.types import JsonType
|
|
29
|
+
from omnibase_infra.models import ModelNodeIdentity
|
|
29
30
|
from omnibase_infra.models.mcp.model_mcp_tool_definition import (
|
|
30
31
|
ModelMCPToolDefinition,
|
|
31
32
|
)
|
|
33
|
+
from omnibase_infra.topics import SUFFIX_NODE_REGISTRATION
|
|
32
34
|
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
36
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
@@ -76,9 +78,8 @@ class ServiceMCPToolSync:
|
|
|
76
78
|
>>> await sync.stop()
|
|
77
79
|
"""
|
|
78
80
|
|
|
79
|
-
# Topic for node registration events
|
|
80
|
-
TOPIC =
|
|
81
|
-
GROUP_ID = "mcp-tool-sync"
|
|
81
|
+
# Topic for node registration events (uses platform suffix constant)
|
|
82
|
+
TOPIC = SUFFIX_NODE_REGISTRATION
|
|
82
83
|
|
|
83
84
|
# MCP tag constants
|
|
84
85
|
TAG_MCP_ENABLED = "mcp-enabled"
|
|
@@ -150,7 +151,6 @@ class ServiceMCPToolSync:
|
|
|
150
151
|
"ServiceMCPToolSync initialized",
|
|
151
152
|
extra={
|
|
152
153
|
"topic": self.TOPIC,
|
|
153
|
-
"group_id": self.GROUP_ID,
|
|
154
154
|
},
|
|
155
155
|
)
|
|
156
156
|
|
|
@@ -172,19 +172,37 @@ class ServiceMCPToolSync:
|
|
|
172
172
|
|
|
173
173
|
correlation_id = uuid4()
|
|
174
174
|
|
|
175
|
+
# OMN-1602: Typed node identity for consumer group derivation.
|
|
176
|
+
#
|
|
177
|
+
# Identity Field Rationale:
|
|
178
|
+
# - env: From bus.environment for deployment-specific consumer groups
|
|
179
|
+
# - service="mcp": MCP is a singleton per environment
|
|
180
|
+
# - node_name="tool_sync": Unique consumer within the MCP service
|
|
181
|
+
# - version="v1": Consumer protocol version (increment on breaking changes)
|
|
182
|
+
sync_identity = ModelNodeIdentity(
|
|
183
|
+
env=self._bus.environment,
|
|
184
|
+
service="mcp",
|
|
185
|
+
node_name="tool_sync",
|
|
186
|
+
version="v1",
|
|
187
|
+
)
|
|
188
|
+
|
|
175
189
|
logger.info(
|
|
176
190
|
"Starting MCP tool sync",
|
|
177
191
|
extra={
|
|
178
192
|
"topic": self.TOPIC,
|
|
179
|
-
"
|
|
193
|
+
"node_identity": {
|
|
194
|
+
"env": sync_identity.env,
|
|
195
|
+
"service": sync_identity.service,
|
|
196
|
+
"node_name": sync_identity.node_name,
|
|
197
|
+
"version": sync_identity.version,
|
|
198
|
+
},
|
|
180
199
|
"correlation_id": str(correlation_id),
|
|
181
200
|
},
|
|
182
201
|
)
|
|
183
202
|
|
|
184
|
-
# Subscribe to registration events
|
|
185
203
|
self._unsubscribe = await self._bus.subscribe(
|
|
186
204
|
topic=self.TOPIC,
|
|
187
|
-
|
|
205
|
+
node_identity=sync_identity,
|
|
188
206
|
on_message=self._on_message,
|
|
189
207
|
)
|
|
190
208
|
|
|
@@ -539,7 +557,7 @@ class ServiceMCPToolSync:
|
|
|
539
557
|
return {
|
|
540
558
|
"service_name": "ServiceMCPToolSync",
|
|
541
559
|
"topic": self.TOPIC,
|
|
542
|
-
"
|
|
560
|
+
"group_id_derived": True, # Group ID derived from ModelNodeIdentity
|
|
543
561
|
"is_running": self._started,
|
|
544
562
|
}
|
|
545
563
|
|
|
@@ -8,6 +8,7 @@ Moved from omniclaude as part of OMN-1526 architectural cleanup.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
+
from typing import Self
|
|
11
12
|
|
|
12
13
|
from pydantic import Field, model_validator
|
|
13
14
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -42,13 +43,8 @@ class ConfigSessionConsumer(BaseSettings):
|
|
|
42
43
|
|
|
43
44
|
# Topics to subscribe
|
|
44
45
|
topics: list[str] = Field(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"dev.omniclaude.session.ended.v1",
|
|
48
|
-
"dev.omniclaude.prompt.submitted.v1",
|
|
49
|
-
"dev.omniclaude.tool.executed.v1",
|
|
50
|
-
],
|
|
51
|
-
description="Kafka topics to consume",
|
|
46
|
+
default_factory=list,
|
|
47
|
+
description="Kafka topics to consume. Must be explicitly configured via environment or discovery.",
|
|
52
48
|
)
|
|
53
49
|
|
|
54
50
|
# Consumer behavior
|
|
@@ -96,7 +92,28 @@ class ConfigSessionConsumer(BaseSettings):
|
|
|
96
92
|
)
|
|
97
93
|
|
|
98
94
|
@model_validator(mode="after")
|
|
99
|
-
def
|
|
95
|
+
def validate_topic_configuration(self) -> Self:
|
|
96
|
+
"""Ensure topics are explicitly configured.
|
|
97
|
+
|
|
98
|
+
Fails fast if no topics provided, preventing silent misconfiguration.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Self if validation passes.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ProtocolConfigurationError: If no topics are configured.
|
|
105
|
+
"""
|
|
106
|
+
if not self.topics:
|
|
107
|
+
from omnibase_infra.errors import ProtocolConfigurationError
|
|
108
|
+
|
|
109
|
+
raise ProtocolConfigurationError(
|
|
110
|
+
"No topics configured for session consumer. "
|
|
111
|
+
"Provide explicit 'topics' via configuration or environment variable."
|
|
112
|
+
)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
@model_validator(mode="after")
|
|
116
|
+
def validate_timing_relationships(self) -> Self:
|
|
100
117
|
"""Validate timing relationships between configuration values.
|
|
101
118
|
|
|
102
119
|
Warns if circuit breaker timeout is very short relative to batch processing,
|
|
@@ -15,7 +15,7 @@ class ConfigSessionStorage(BaseSettings):
|
|
|
15
15
|
"""Configuration for session snapshot PostgreSQL storage.
|
|
16
16
|
|
|
17
17
|
Environment variables use the OMNIBASE_INFRA_SESSION_STORAGE_ prefix.
|
|
18
|
-
Example: OMNIBASE_INFRA_SESSION_STORAGE_POSTGRES_HOST=
|
|
18
|
+
Example: OMNIBASE_INFRA_SESSION_STORAGE_POSTGRES_HOST=db.example.com
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
model_config = SettingsConfigDict(
|
|
@@ -28,7 +28,7 @@ class ConfigSessionStorage(BaseSettings):
|
|
|
28
28
|
|
|
29
29
|
# PostgreSQL connection
|
|
30
30
|
postgres_host: str = Field(
|
|
31
|
-
default="
|
|
31
|
+
default="localhost",
|
|
32
32
|
description="PostgreSQL host",
|
|
33
33
|
)
|
|
34
34
|
postgres_port: int = Field(
|
|
@@ -257,7 +257,7 @@ class SessionEventConsumer:
|
|
|
257
257
|
|
|
258
258
|
Example:
|
|
259
259
|
>>> config = ConfigSessionConsumer(
|
|
260
|
-
... bootstrap_servers="
|
|
260
|
+
... bootstrap_servers="localhost:9092",
|
|
261
261
|
... group_id="my-consumer-group",
|
|
262
262
|
... )
|
|
263
263
|
>>> aggregator = InMemorySessionAggregator()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""ONEX Infrastructure Topic Constants.
|
|
2
|
+
|
|
3
|
+
This module provides platform-reserved topic suffix constants for ONEX infrastructure
|
|
4
|
+
components. Domain services should NOT import from this module - domain topics should
|
|
5
|
+
be defined in domain contracts.
|
|
6
|
+
|
|
7
|
+
Exports:
|
|
8
|
+
Platform topic suffix constants (e.g., SUFFIX_NODE_REGISTRATION)
|
|
9
|
+
ALL_PLATFORM_SUFFIXES: Complete tuple of all platform-reserved suffixes
|
|
10
|
+
build_full_topic: Compose full topic from env, namespace, and suffix
|
|
11
|
+
TopicCompositionError: Error raised when topic composition fails
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from omnibase_infra.topics.platform_topic_suffixes import (
|
|
15
|
+
ALL_PLATFORM_SUFFIXES,
|
|
16
|
+
SUFFIX_FSM_STATE_TRANSITIONS,
|
|
17
|
+
SUFFIX_NODE_HEARTBEAT,
|
|
18
|
+
SUFFIX_NODE_INTROSPECTION,
|
|
19
|
+
SUFFIX_NODE_REGISTRATION,
|
|
20
|
+
SUFFIX_REGISTRATION_SNAPSHOTS,
|
|
21
|
+
SUFFIX_REQUEST_INTROSPECTION,
|
|
22
|
+
SUFFIX_RUNTIME_TICK,
|
|
23
|
+
)
|
|
24
|
+
from omnibase_infra.topics.util_topic_composition import (
|
|
25
|
+
MAX_NAMESPACE_LENGTH,
|
|
26
|
+
TopicCompositionError,
|
|
27
|
+
build_full_topic,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__: list[str] = [
|
|
31
|
+
# Individual suffix constants
|
|
32
|
+
"SUFFIX_NODE_REGISTRATION",
|
|
33
|
+
"SUFFIX_NODE_INTROSPECTION",
|
|
34
|
+
"SUFFIX_NODE_HEARTBEAT",
|
|
35
|
+
"SUFFIX_REQUEST_INTROSPECTION",
|
|
36
|
+
"SUFFIX_FSM_STATE_TRANSITIONS",
|
|
37
|
+
"SUFFIX_RUNTIME_TICK",
|
|
38
|
+
"SUFFIX_REGISTRATION_SNAPSHOTS",
|
|
39
|
+
# Aggregate tuple
|
|
40
|
+
"ALL_PLATFORM_SUFFIXES",
|
|
41
|
+
# Topic composition utilities
|
|
42
|
+
"build_full_topic",
|
|
43
|
+
"TopicCompositionError",
|
|
44
|
+
"MAX_NAMESPACE_LENGTH",
|
|
45
|
+
]
|