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.
Files changed (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,140 @@
1
+ """Platform-reserved topic suffixes for ONEX infrastructure.
2
+
3
+ WARNING: These are platform-reserved suffixes. Domain services must NOT
4
+ import from this module. Domain topics should be defined in domain contracts.
5
+
6
+ Topic Suffix Format:
7
+ onex.<kind>.<producer>.<event-name>.v<version>
8
+
9
+ Structure:
10
+ - onex: Required prefix for all ONEX topics
11
+ - kind: Message category (evt, cmd, intent, snapshot, dlq)
12
+ - producer: Service/module that produces the message
13
+ - event-name: Descriptive name using kebab-case
14
+ - version: Semantic version (v1, v2, etc.)
15
+
16
+ Kinds:
17
+ evt - Event topics (state changes, notifications)
18
+ cmd - Command topics (requests for action)
19
+ intent - Intent topics (internal workflow coordination)
20
+ snapshot - Snapshot topics (periodic state snapshots)
21
+ dlq - Dead letter queue topics
22
+
23
+ Examples:
24
+ onex.evt.platform.node-registration.v1
25
+ onex.cmd.platform.request-introspection.v1
26
+ onex.intent.platform.runtime-tick.v1
27
+
28
+ Usage:
29
+ from omnibase_infra.topics import SUFFIX_NODE_REGISTRATION
30
+
31
+ # Compose full topic with tenant/namespace prefix
32
+ full_topic = f"{tenant}.{namespace}.{SUFFIX_NODE_REGISTRATION}"
33
+
34
+ See Also:
35
+ omnibase_core.validation.validate_topic_suffix - Validation function
36
+ omnibase_core.validation.compose_full_topic - Topic composition utility
37
+ """
38
+
39
+ from omnibase_core.errors import OnexError
40
+ from omnibase_core.validation import validate_topic_suffix
41
+
42
+ # =============================================================================
43
+ # PLATFORM-RESERVED TOPIC SUFFIXES
44
+ # =============================================================================
45
+
46
+ # Node lifecycle events
47
+ SUFFIX_NODE_REGISTRATION: str = "onex.evt.platform.node-registration.v1"
48
+ """Topic suffix for node registration events.
49
+
50
+ Published when a node registers with the runtime. Contains node metadata,
51
+ capabilities, and health check configuration.
52
+ """
53
+
54
+ SUFFIX_NODE_INTROSPECTION: str = "onex.evt.platform.node-introspection.v1"
55
+ """Topic suffix for node introspection events.
56
+
57
+ Published when a node responds to an introspection request. Contains node
58
+ capabilities, supported operations, and current state.
59
+ """
60
+
61
+ SUFFIX_NODE_HEARTBEAT: str = "onex.evt.platform.node-heartbeat.v1"
62
+ """Topic suffix for node heartbeat events.
63
+
64
+ Published periodically by nodes to indicate liveness. Contains timestamp,
65
+ resource usage metrics, and health status.
66
+ """
67
+
68
+ # Command topics
69
+ SUFFIX_REQUEST_INTROSPECTION: str = "onex.cmd.platform.request-introspection.v1"
70
+ """Topic suffix for introspection request commands.
71
+
72
+ Published to request introspection from a specific node or all nodes.
73
+ Nodes respond on the SUFFIX_NODE_INTROSPECTION topic.
74
+ """
75
+
76
+ # FSM and state management
77
+ SUFFIX_FSM_STATE_TRANSITIONS: str = "onex.evt.platform.fsm-state-transitions.v1"
78
+ """Topic suffix for FSM state transition events.
79
+
80
+ Published when a node's finite state machine transitions between states.
81
+ Contains previous state, new state, trigger event, and transition metadata.
82
+ """
83
+
84
+ # Runtime coordination
85
+ SUFFIX_RUNTIME_TICK: str = "onex.intent.platform.runtime-tick.v1"
86
+ """Topic suffix for runtime tick intents.
87
+
88
+ Internal topic for runtime orchestration. Triggers periodic tasks like
89
+ heartbeat collection, health checks, and scheduled workflows.
90
+ """
91
+
92
+ # Registration snapshots
93
+ SUFFIX_REGISTRATION_SNAPSHOTS: str = "onex.snapshot.platform.registration-snapshots.v1"
94
+ """Topic suffix for registration snapshot events.
95
+
96
+ Published periodically with aggregated registration state. Used for
97
+ dashboard displays and monitoring systems.
98
+ """
99
+
100
+ # =============================================================================
101
+ # AGGREGATE TUPLE
102
+ # =============================================================================
103
+
104
+ ALL_PLATFORM_SUFFIXES: tuple[str, ...] = (
105
+ SUFFIX_NODE_REGISTRATION,
106
+ SUFFIX_NODE_INTROSPECTION,
107
+ SUFFIX_NODE_HEARTBEAT,
108
+ SUFFIX_REQUEST_INTROSPECTION,
109
+ SUFFIX_FSM_STATE_TRANSITIONS,
110
+ SUFFIX_RUNTIME_TICK,
111
+ SUFFIX_REGISTRATION_SNAPSHOTS,
112
+ )
113
+ """Complete tuple of all platform-reserved topic suffixes.
114
+
115
+ Use this tuple for:
116
+ - Validating that domain topics don't conflict with platform topics
117
+ - Iterating over all platform topics for subscription setup
118
+ - Documentation and discovery
119
+ """
120
+
121
+ # =============================================================================
122
+ # IMPORT-TIME VALIDATION
123
+ # =============================================================================
124
+
125
+
126
+ def _validate_all_suffixes() -> None:
127
+ """Validate all suffixes at import time to fail fast on invalid format.
128
+
129
+ Raises:
130
+ OnexError: If any suffix fails validation with details about which
131
+ suffix failed and why.
132
+ """
133
+ for suffix in ALL_PLATFORM_SUFFIXES:
134
+ result = validate_topic_suffix(suffix)
135
+ if not result.is_valid:
136
+ raise OnexError(f"Invalid platform topic suffix '{suffix}': {result.error}")
137
+
138
+
139
+ # Run validation at import time
140
+ _validate_all_suffixes()
@@ -0,0 +1,95 @@
1
+ """Topic composition utilities for ONEX infrastructure.
2
+
3
+ IMPORTANT: build_full_topic() is the ONLY supported way to compose
4
+ Kafka topics in omnibase_infra. Direct string concatenation is prohibited.
5
+ """
6
+
7
+ from omnibase_core.errors import OnexError
8
+ from omnibase_core.validation import validate_topic_suffix
9
+ from omnibase_core.validation.validator_topic_suffix import ENV_PREFIXES
10
+
11
+ MAX_NAMESPACE_LENGTH = 100
12
+ """Maximum allowed namespace length.
13
+
14
+ Kafka topics have a 249 character limit. With env prefix (~10 chars),
15
+ separators (2 dots), and suffix (~50 chars), namespace should be limited
16
+ to ensure total topic length stays within bounds.
17
+ """
18
+
19
+
20
+ class TopicCompositionError(OnexError):
21
+ """Raised when topic composition fails due to invalid components.
22
+
23
+ Extends OnexError to follow ONEX error handling conventions.
24
+ """
25
+
26
+
27
+ def build_full_topic(env: str, namespace: str, suffix: str) -> str:
28
+ """Build full topic from components with validation.
29
+
30
+ Args:
31
+ env: Environment prefix (e.g., "dev", "staging", "prod", "test", "local")
32
+ namespace: Namespace/tenant identifier (e.g., "omnibase", "myapp", "tenant-123")
33
+ suffix: Validated topic suffix (e.g., "onex.evt.platform.node-introspection.v1")
34
+
35
+ Returns:
36
+ Full topic string: {env}.{namespace}.{suffix}
37
+
38
+ Raises:
39
+ TopicCompositionError: If env is not a valid environment prefix
40
+ TopicCompositionError: If namespace is empty or contains invalid characters
41
+ TopicCompositionError: If suffix doesn't match ONEX topic format
42
+
43
+ Namespace Validation Rules:
44
+ - Must not be empty
45
+ - Allowed characters: alphanumeric (a-z, A-Z, 0-9), hyphens (-), underscores (_)
46
+ - Numeric-only namespaces ARE valid (e.g., "12345") to support numeric tenant IDs
47
+ - Invalid: spaces, dots, special characters
48
+
49
+ Example:
50
+ >>> build_full_topic("dev", "omnibase", "onex.evt.platform.node-introspection.v1")
51
+ 'dev.omnibase.onex.evt.platform.node-introspection.v1'
52
+
53
+ >>> build_full_topic("prod", "myapp", "onex.cmd.platform.request-introspection.v1")
54
+ 'prod.myapp.onex.cmd.platform.request-introspection.v1'
55
+ """
56
+ # Validate environment prefix
57
+ if env not in ENV_PREFIXES:
58
+ raise TopicCompositionError(
59
+ f"Invalid environment prefix '{env}'. "
60
+ f"Must be one of: {', '.join(sorted(ENV_PREFIXES))}"
61
+ )
62
+
63
+ # Validate namespace
64
+ # Allowed characters: alphanumeric (a-z, A-Z, 0-9), hyphens, underscores
65
+ # Note: Numeric-only namespaces (e.g., "12345") ARE valid because isalnum()
66
+ # returns True for digit-only strings. This is intentional to support
67
+ # numeric tenant/organization IDs as namespaces.
68
+ if not namespace:
69
+ raise TopicCompositionError("Namespace cannot be empty")
70
+ if len(namespace) > MAX_NAMESPACE_LENGTH:
71
+ raise TopicCompositionError(
72
+ f"Namespace exceeds maximum length of {MAX_NAMESPACE_LENGTH} characters. "
73
+ f"Got {len(namespace)} characters."
74
+ )
75
+ if not namespace.replace("-", "").replace("_", "").isalnum():
76
+ raise TopicCompositionError(
77
+ f"Invalid namespace '{namespace}'. "
78
+ "Must contain only alphanumeric characters, hyphens, and underscores"
79
+ )
80
+
81
+ # Enforce lowercase to ensure composed topics are valid
82
+ # (ONEX topic suffixes are lowercase, so namespaces should match)
83
+ if namespace != namespace.lower():
84
+ raise TopicCompositionError(
85
+ f"Namespace must be lowercase: '{namespace}'. "
86
+ "Use lowercase to ensure consistent topic naming."
87
+ )
88
+
89
+ # Validate suffix using omnibase_core validation
90
+ result = validate_topic_suffix(suffix)
91
+ if not result.is_valid:
92
+ raise TopicCompositionError(f"Invalid topic suffix '{suffix}': {result.error}")
93
+
94
+ # Compose full topic
95
+ return f"{env}.{namespace}.{suffix}"
@@ -7,10 +7,14 @@ forms of Pydantic models, enabling proper type checking for cache operations
7
7
  and JSON serialization/deserialization without requiring type: ignore comments.
8
8
 
9
9
  Available TypedDicts:
10
+ - TypedDictEnvelopeBuildParams: Parameters for building ModelEventEnvelope
10
11
  - TypedDictIntrospectionCache: JSON-serialized ModelNodeIntrospectionEvent
11
12
  - TypedDictPerformanceMetricsCache: JSON-serialized introspection performance metrics
12
13
  """
13
14
 
15
+ from omnibase_infra.types.typed_dict.typed_dict_envelope_build_params import (
16
+ TypedDictEnvelopeBuildParams,
17
+ )
14
18
  from omnibase_infra.types.typed_dict.typed_dict_introspection_cache import (
15
19
  TypedDictIntrospectionCache,
16
20
  )
@@ -18,4 +22,8 @@ from omnibase_infra.types.typed_dict.typed_dict_performance_metrics_cache import
18
22
  TypedDictPerformanceMetricsCache,
19
23
  )
20
24
 
21
- __all__ = ["TypedDictIntrospectionCache", "TypedDictPerformanceMetricsCache"]
25
+ __all__ = [
26
+ "TypedDictEnvelopeBuildParams",
27
+ "TypedDictIntrospectionCache",
28
+ "TypedDictPerformanceMetricsCache",
29
+ ]
@@ -0,0 +1,115 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """TypedDict definition for envelope build parameters.
4
+
5
+ This module provides a TypedDict that represents the parameters passed to the
6
+ _build_envelope method in AdapterProtocolEventPublisherInmemory, replacing the
7
+ loose dict[str, object] typing with proper type safety.
8
+
9
+ The TypedDictEnvelopeBuildParams ensures type-checked access to envelope
10
+ construction parameters without requiring type: ignore comments or unsafe casts.
11
+
12
+ Key Features:
13
+ - Full type annotations for all envelope build parameters
14
+ - Proper handling of nullable fields (correlation_id, causation_id, metadata)
15
+ - Integration with JsonType for payload typing
16
+ - Uses TYPE_CHECKING import for ContextValue to avoid circular imports
17
+
18
+ Usage:
19
+ This TypedDict is primarily used for:
20
+ - Type-safe parameter passing to envelope builder methods
21
+ - Eliminating dict[str, object] loose typing
22
+ - Enabling mypy verification of parameter access
23
+
24
+ Example:
25
+ ```python
26
+ from omnibase_infra.types.typed_dict import TypedDictEnvelopeBuildParams
27
+
28
+ def build_envelope(params: TypedDictEnvelopeBuildParams) -> ModelEventEnvelope:
29
+ # Type-safe access to all fields
30
+ event_type = params["event_type"]
31
+ payload = params["payload"]
32
+ correlation_id = params.get("correlation_id")
33
+ return ...
34
+ ```
35
+
36
+ See Also:
37
+ - AdapterProtocolEventPublisherInmemory: Primary consumer of this TypedDict
38
+ - ModelEventEnvelope: Target envelope model constructed from these params
39
+ - ProtocolEventPublisher: SPI protocol defining publish interface
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import TYPE_CHECKING, NotRequired, TypedDict
45
+
46
+ from omnibase_core.types import JsonType
47
+
48
+ if TYPE_CHECKING:
49
+ from omnibase_spi.protocols.types.protocol_core_types import ContextValue
50
+
51
+ __all__ = ["TypedDictEnvelopeBuildParams"]
52
+
53
+
54
+ class TypedDictEnvelopeBuildParams(TypedDict):
55
+ """TypedDict representing parameters for building a ModelEventEnvelope.
56
+
57
+ This type provides a type-safe alternative to dict[str, object] for passing
58
+ envelope construction parameters. It enables proper type checking when
59
+ building event envelopes in publisher adapters.
60
+
61
+ Required fields (event_type, payload) are enforced by TypedDict's default
62
+ total=True behavior. Optional fields use NotRequired[] to allow omission
63
+ while maintaining type safety.
64
+
65
+ Attributes:
66
+ event_type: Fully-qualified event type identifier
67
+ (e.g., "omninode.user.event.created.v1"). Required field - must
68
+ always be provided when constructing the TypedDict.
69
+ payload: Event payload data as JSON-compatible types. Required field -
70
+ must always be provided. Can be dict, list, str, int, float, bool,
71
+ or None.
72
+ correlation_id: Optional correlation ID for request tracing.
73
+ Used to link related events across service boundaries.
74
+ May be omitted or set to None.
75
+ causation_id: Optional causation ID for event sourcing chains.
76
+ Links this event to the event that caused it.
77
+ May be omitted or set to None.
78
+ metadata: Optional additional context values for the envelope.
79
+ Keys are string identifiers, values are ContextValue protocol
80
+ implementations (e.g., ProtocolContextStringValue).
81
+ May be omitted or set to None.
82
+
83
+ Note:
84
+ The metadata field uses ContextValue from omnibase_spi which is imported
85
+ under TYPE_CHECKING to avoid runtime circular import issues. At runtime,
86
+ the dict values are duck-typed protocol implementations.
87
+
88
+ Example:
89
+ ```python
90
+ # Minimal valid TypedDict (only required fields)
91
+ params: TypedDictEnvelopeBuildParams = {
92
+ "event_type": "user.created.v1",
93
+ "payload": {"user_id": "123", "email": "user@example.com"},
94
+ }
95
+
96
+ # Full TypedDict with all fields
97
+ params_full: TypedDictEnvelopeBuildParams = {
98
+ "event_type": "user.created.v1",
99
+ "payload": {"user_id": "123", "email": "user@example.com"},
100
+ "correlation_id": "corr-abc-123",
101
+ "causation_id": None,
102
+ "metadata": {"trace_id": trace_value},
103
+ }
104
+
105
+ # Safe field access
106
+ event_type: str = params["event_type"] # Always present
107
+ correlation_id: str | None = params.get("correlation_id") # May be absent
108
+ ```
109
+ """
110
+
111
+ event_type: str
112
+ payload: JsonType
113
+ correlation_id: NotRequired[str | None]
114
+ causation_id: NotRequired[str | None]
115
+ metadata: NotRequired[dict[str, ContextValue] | None]
@@ -5,6 +5,7 @@
5
5
  This package provides common utilities used across the infrastructure:
6
6
  - correlation: Correlation ID generation and propagation for distributed tracing
7
7
  - util_atomic_file: Atomic file write primitives using temp-file-rename pattern
8
+ - util_consumer_group: Kafka consumer group ID generation with deterministic hashing
8
9
  - util_datetime: Datetime validation and timezone normalization
9
10
  - util_db_transaction: Database transaction context manager for asyncpg
10
11
  - util_dsn_validation: PostgreSQL DSN validation and sanitization
@@ -26,6 +27,11 @@ from omnibase_infra.utils.util_atomic_file import (
26
27
  write_atomic_bytes,
27
28
  write_atomic_bytes_async,
28
29
  )
30
+ from omnibase_infra.utils.util_consumer_group import (
31
+ KAFKA_CONSUMER_GROUP_MAX_LENGTH,
32
+ compute_consumer_group_id,
33
+ normalize_kafka_identifier,
34
+ )
29
35
  from omnibase_infra.utils.util_datetime import (
30
36
  ensure_timezone_aware,
31
37
  is_timezone_aware,
@@ -72,15 +78,18 @@ from omnibase_infra.utils.util_semver import (
72
78
 
73
79
  __all__: list[str] = [
74
80
  "CorrelationContext",
81
+ "KAFKA_CONSUMER_GROUP_MAX_LENGTH",
75
82
  "OptimisticConflictError",
76
83
  "SAFE_ERROR_PATTERNS",
77
84
  "SEMVER_PATTERN",
78
85
  "SENSITIVE_PATTERNS",
79
86
  "clear_correlation_id",
87
+ "compute_consumer_group_id",
80
88
  "ensure_timezone_aware",
81
89
  "generate_correlation_id",
82
90
  "get_correlation_id",
83
91
  "is_timezone_aware",
92
+ "normalize_kafka_identifier",
84
93
  "parse_and_validate_dsn",
85
94
  "parse_env_float",
86
95
  "parse_env_int",
@@ -0,0 +1,232 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Kafka consumer group ID utilities.
4
+
5
+ Provides utilities for normalizing and validating Kafka consumer group identifiers.
6
+ Kafka consumer group IDs have specific character and length constraints that this
7
+ module helps enforce consistently across the codebase.
8
+
9
+ Kafka Consumer Group ID Constraints:
10
+ - Maximum length: 255 characters
11
+ - Valid characters: alphanumeric, period (.), underscore (_), hyphen (-)
12
+ - Cannot be empty
13
+
14
+ This module provides:
15
+ - normalize_kafka_identifier: Normalize strings for use as Kafka consumer group IDs
16
+ - compute_consumer_group_id: Compute canonical consumer group ID from node identity
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import re
23
+ from typing import TYPE_CHECKING
24
+
25
+ from omnibase_infra.enums import EnumConsumerGroupPurpose
26
+
27
+ if TYPE_CHECKING:
28
+ from omnibase_infra.models import ModelNodeIdentity
29
+
30
+ # Maximum length for Kafka consumer group IDs
31
+ KAFKA_CONSUMER_GROUP_MAX_LENGTH = 255
32
+
33
+ # Pattern for invalid characters (anything not alphanumeric, period, underscore, or hyphen)
34
+ _INVALID_CHAR_PATTERN = re.compile(r"[^a-z0-9._-]")
35
+
36
+ # Pattern for consecutive separators (period, underscore, or hyphen)
37
+ _CONSECUTIVE_SEPARATOR_PATTERN = re.compile(r"[._-]{2,}")
38
+
39
+ # Pattern for leading/trailing separators
40
+ _EDGE_SEPARATOR_PATTERN = re.compile(r"^[._-]+|[._-]+$")
41
+
42
+
43
+ def normalize_kafka_identifier(value: str) -> str:
44
+ """Normalize a string for use as a Kafka consumer group ID.
45
+
46
+ Applies the following transformations in order:
47
+ 1. Convert to lowercase
48
+ 2. Replace invalid characters (non-alphanumeric, non-separator) with underscore
49
+ 3. Collapse consecutive separators (., _, -) into a single separator
50
+ 4. Strip leading and trailing separators
51
+ 5. Truncate to max length (255) with hash suffix if necessary
52
+
53
+ The hash suffix ensures uniqueness when truncation is required. The suffix
54
+ format is `_<8-char-hash>` appended after truncating to fit within 255 chars.
55
+
56
+ Args:
57
+ value: The input string to normalize.
58
+
59
+ Returns:
60
+ A normalized string safe for use as a Kafka consumer group ID.
61
+
62
+ Raises:
63
+ ValueError: If the input is empty or results in an empty string after
64
+ normalization.
65
+
66
+ Example:
67
+ >>> normalize_kafka_identifier("My Service!!")
68
+ 'my_service'
69
+ >>> normalize_kafka_identifier("foo..bar__baz")
70
+ 'foo.bar_baz'
71
+ >>> normalize_kafka_identifier(" UPPER_Case-Test ")
72
+ 'upper_case-test'
73
+ >>> normalize_kafka_identifier("valid.consumer-group_id")
74
+ 'valid.consumer-group_id'
75
+ >>> normalize_kafka_identifier("@#$%^&*()") # Raises ValueError
76
+ Traceback (most recent call last):
77
+ ...
78
+ ValueError: Input '@#$%^&*()' results in empty string after normalization
79
+ """
80
+ if not value:
81
+ raise ValueError("Kafka consumer group ID cannot be empty")
82
+
83
+ # Step 1: Lowercase
84
+ result = value.lower()
85
+
86
+ # Step 2: Replace invalid characters with underscore
87
+ result = _INVALID_CHAR_PATTERN.sub("_", result)
88
+
89
+ # Step 3: Collapse consecutive separators, preserving the first separator type
90
+ result = _CONSECUTIVE_SEPARATOR_PATTERN.sub(lambda m: m.group(0)[0], result)
91
+
92
+ # Step 4: Strip leading and trailing separators
93
+ result = _EDGE_SEPARATOR_PATTERN.sub("", result)
94
+
95
+ # Check for empty result after normalization
96
+ if not result:
97
+ raise ValueError(f"Input {value!r} results in empty string after normalization")
98
+
99
+ # Step 5: Truncate with hash suffix if exceeds max length
100
+ if len(result) > KAFKA_CONSUMER_GROUP_MAX_LENGTH:
101
+ # Generate 8-character hash suffix from original value for determinism
102
+ hash_suffix = hashlib.sha256(value.encode()).hexdigest()[:8]
103
+ # Reserve space for underscore + hash suffix (9 chars total)
104
+ max_prefix_length = KAFKA_CONSUMER_GROUP_MAX_LENGTH - 9
105
+ result = f"{result[:max_prefix_length]}_{hash_suffix}"
106
+
107
+ return result
108
+
109
+
110
+ def compute_consumer_group_id(
111
+ identity: ModelNodeIdentity,
112
+ purpose: EnumConsumerGroupPurpose = EnumConsumerGroupPurpose.CONSUME,
113
+ ) -> str:
114
+ """Compute canonical Kafka consumer group ID from node identity.
115
+
116
+ Generates a deterministic, Kafka-compliant consumer group ID using the
117
+ canonical format: ``{env}.{service}.{node_name}.{purpose}.{version}``
118
+
119
+ Each component is normalized using ``normalize_kafka_identifier()`` to ensure
120
+ the result is safe for use as a Kafka consumer group ID. The final result
121
+ is validated against Kafka's 255 character limit.
122
+
123
+ Args:
124
+ identity: Node identity containing env, service, node_name, and version.
125
+ purpose: Consumer group purpose classification. Defaults to CONSUME.
126
+ The purpose determines consumer behavior semantics (e.g., offset
127
+ reset policy) and is included in the group ID for disambiguation.
128
+
129
+ Returns:
130
+ A canonical consumer group ID in the format:
131
+ ``{env}.{service}.{node_name}.{purpose}.{version}``
132
+
133
+ If the combined length exceeds Kafka's 255 character limit, the result
134
+ is truncated with an 8-character hash suffix to preserve uniqueness
135
+ while fitting within the constraint.
136
+
137
+ Example:
138
+ >>> from omnibase_infra.models import ModelNodeIdentity
139
+ >>> from omnibase_infra.enums import EnumConsumerGroupPurpose
140
+ >>> identity = ModelNodeIdentity(
141
+ ... env="dev",
142
+ ... service="omniintelligence",
143
+ ... node_name="claude_hook_event_effect",
144
+ ... version="v1",
145
+ ... )
146
+ >>> compute_consumer_group_id(identity)
147
+ 'dev.omniintelligence.claude_hook_event_effect.consume.v1'
148
+
149
+ With a different purpose:
150
+
151
+ >>> compute_consumer_group_id(identity, EnumConsumerGroupPurpose.INTROSPECTION)
152
+ 'dev.omniintelligence.claude_hook_event_effect.introspection.v1'
153
+
154
+ Component normalization is applied automatically:
155
+
156
+ >>> identity_mixed = ModelNodeIdentity(
157
+ ... env="DEV",
158
+ ... service="Omni Intelligence",
159
+ ... node_name="claude-hook-event-effect",
160
+ ... version="V1.0.0",
161
+ ... )
162
+ >>> compute_consumer_group_id(identity_mixed)
163
+ 'dev.omni_intelligence.claude-hook-event-effect.consume.v1.0.0'
164
+
165
+ Long identities are automatically truncated with a hash suffix:
166
+
167
+ >>> long_identity = ModelNodeIdentity(
168
+ ... env="development",
169
+ ... service="a" * 100,
170
+ ... node_name="b" * 100,
171
+ ... version="v1",
172
+ ... )
173
+ >>> result = compute_consumer_group_id(long_identity)
174
+ >>> len(result) <= 255
175
+ True
176
+ >>> result.endswith("_" + result[-8:]) # Has hash suffix
177
+ True
178
+
179
+ Note:
180
+ The canonical format uses period (.) as the separator between components.
181
+ This enables hierarchical grouping and filtering in Kafka tooling while
182
+ maintaining compatibility with Kafka's consumer group ID constraints.
183
+
184
+ See Also:
185
+ - :func:`normalize_kafka_identifier`: Component normalization rules
186
+ - :class:`~omnibase_infra.enums.EnumConsumerGroupPurpose`: Purpose values
187
+ - :class:`~omnibase_infra.models.ModelNodeIdentity`: Identity model
188
+
189
+ .. versionadded:: 0.2.6
190
+ Created as part of OMN-1602.
191
+ """
192
+ # Normalize each component
193
+ normalized_env = normalize_kafka_identifier(identity.env)
194
+ normalized_service = normalize_kafka_identifier(identity.service)
195
+ normalized_node_name = normalize_kafka_identifier(identity.node_name)
196
+ normalized_purpose = normalize_kafka_identifier(purpose.value)
197
+ normalized_version = normalize_kafka_identifier(identity.version)
198
+
199
+ # Join with period separator
200
+ group_id = ".".join(
201
+ [
202
+ normalized_env,
203
+ normalized_service,
204
+ normalized_node_name,
205
+ normalized_purpose,
206
+ normalized_version,
207
+ ]
208
+ )
209
+
210
+ # Handle length constraint with truncation + hash (same strategy as normalize_kafka_identifier)
211
+ # This can occur when multiple long components combine, even though each was individually
212
+ # truncated to 255 chars by normalize_kafka_identifier.
213
+ if len(group_id) > KAFKA_CONSUMER_GROUP_MAX_LENGTH:
214
+ # Generate deterministic hash from original (pre-normalized) identity components
215
+ # to ensure the same identity always produces the same truncated group ID
216
+ hash_input = (
217
+ f"{identity.env}|{identity.service}|{identity.node_name}|"
218
+ f"{purpose.value}|{identity.version}"
219
+ )
220
+ hash_suffix = hashlib.sha256(hash_input.encode()).hexdigest()[:8]
221
+ # Reserve space for underscore + hash suffix (9 chars total)
222
+ max_prefix_length = KAFKA_CONSUMER_GROUP_MAX_LENGTH - 9
223
+ group_id = f"{group_id[:max_prefix_length]}_{hash_suffix}"
224
+
225
+ return group_id
226
+
227
+
228
+ __all__: list[str] = [
229
+ "KAFKA_CONSUMER_GROUP_MAX_LENGTH",
230
+ "compute_consumer_group_id",
231
+ "normalize_kafka_identifier",
232
+ ]
@@ -429,7 +429,24 @@ INFRA_NODES_PATH = "src/omnibase_infra/nodes/"
429
429
  # ast.FunctionDef | ast.AsyncFunctionDef for AST method type checking
430
430
  # - 105 (2026-01-21): Contract-driven handler config loading (+4 unions)
431
431
  # ModelHandlerContract transport config fields and lifecycle types
432
- INFRA_MAX_UNIONS = 105
432
+ # - 108 (2026-01-27): OMN-1518 declarative operation bindings (+3 unions)
433
+ # ModelEventEnvelope[object] | dict[str, object] for materialized
434
+ # envelopes in dispatch engine (3 occurrences in type aliases)
435
+ # - 105 (2026-01-27): OMN-1518 simplify to always-dict envelope format (-3 unions)
436
+ # Removed hybrid union types by always materializing to dict format
437
+ # Dispatchers now receive consistent dict[str, object] with __bindings
438
+ # - 112 (2026-01-27): OMN-1610 emit daemon for persistent Kafka connections (+7 unions)
439
+ # BoundedEventQueue, EmitClient, EventRegistry return types
440
+ # - 112 (2026-01-29): OMN-1610 emit daemon + refactor to strongly typed input model
441
+ # BoundedEventQueue, EmitClient, EventRegistry, socket_permissions
442
+ # Replaced dict unions with ModelEmitDaemonConfigInput
443
+ # - 113 (2026-01-29): OMN-1610 properly typed daemon protocol models (+1 union)
444
+ # Added ModelDaemonRequest, ModelDaemonResponse discriminated unions
445
+ # Replaced dict[str, object] soup with strongly typed Pydantic models
446
+ # - 115 (2026-01-29): OMN-1653 contract registry reducer (+2 unions)
447
+ # ContractRegistryEvent: 4-type union for event routing
448
+ # contract_yaml: dict | str for flexible YAML handling
449
+ INFRA_MAX_UNIONS = 115
433
450
 
434
451
  # Maximum allowed architecture violations in infrastructure code.
435
452
  # Set to 0 (strict enforcement) to ensure one-model-per-file principle is always followed.