omnibase_infra 0.2.1__py3-none-any.whl → 0.2.3__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 (161) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/commands.py +1 -1
  8. omnibase_infra/configs/widget_mapping.yaml +176 -0
  9. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
  10. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
  11. omnibase_infra/enums/__init__.py +6 -0
  12. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  13. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  14. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  15. omnibase_infra/errors/error_compute_registry.py +4 -1
  16. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  17. omnibase_infra/errors/error_infra.py +3 -1
  18. omnibase_infra/errors/error_policy_registry.py +4 -1
  19. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  20. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  21. omnibase_infra/handlers/__init__.py +8 -1
  22. omnibase_infra/handlers/handler_consul.py +7 -1
  23. omnibase_infra/handlers/handler_db.py +10 -3
  24. omnibase_infra/handlers/handler_graph.py +10 -5
  25. omnibase_infra/handlers/handler_http.py +8 -2
  26. omnibase_infra/handlers/handler_intent.py +387 -0
  27. omnibase_infra/handlers/handler_mcp.py +745 -63
  28. omnibase_infra/handlers/handler_vault.py +11 -5
  29. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  30. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  31. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  32. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
  33. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  34. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  35. omnibase_infra/mixins/mixin_node_introspection.py +42 -7
  36. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  37. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  38. omnibase_infra/models/handlers/__init__.py +48 -5
  39. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  40. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  41. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  42. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  43. omnibase_infra/models/mcp/__init__.py +15 -0
  44. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  45. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  46. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  47. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  48. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  49. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  50. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  51. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  52. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  53. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  54. omnibase_infra/nodes/effects/contract.yaml +0 -5
  55. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  56. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  57. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  58. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  59. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  60. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  61. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  62. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  63. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  64. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
  65. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  66. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  67. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
  68. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  69. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  70. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  71. omnibase_infra/protocols/__init__.py +2 -0
  72. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  73. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  74. omnibase_infra/runtime/__init__.py +90 -1
  75. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  76. omnibase_infra/runtime/constants_notification.py +75 -0
  77. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  78. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  79. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  80. omnibase_infra/runtime/handler_contract_source.py +267 -186
  81. omnibase_infra/runtime/handler_identity.py +81 -0
  82. omnibase_infra/runtime/handler_plugin_loader.py +19 -2
  83. omnibase_infra/runtime/handler_registry.py +11 -3
  84. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  85. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  86. omnibase_infra/runtime/mixins/__init__.py +7 -0
  87. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  88. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  89. omnibase_infra/runtime/models/__init__.py +24 -0
  90. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  91. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  92. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  93. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  94. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  95. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  96. omnibase_infra/runtime/projector_shell.py +229 -1
  97. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  98. omnibase_infra/runtime/protocols/__init__.py +10 -0
  99. omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
  100. omnibase_infra/runtime/registry_contract_source.py +693 -0
  101. omnibase_infra/runtime/registry_policy.py +9 -326
  102. omnibase_infra/runtime/secret_resolver.py +4 -2
  103. omnibase_infra/runtime/service_kernel.py +11 -3
  104. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  105. omnibase_infra/runtime/service_runtime_host_process.py +589 -106
  106. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  107. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  108. omnibase_infra/runtime/util_container_wiring.py +6 -5
  109. omnibase_infra/runtime/util_wiring.py +17 -4
  110. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  111. omnibase_infra/services/__init__.py +21 -0
  112. omnibase_infra/services/corpus_capture.py +7 -1
  113. omnibase_infra/services/mcp/__init__.py +31 -0
  114. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  115. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  116. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  117. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  118. omnibase_infra/services/registry_api/__init__.py +40 -0
  119. omnibase_infra/services/registry_api/main.py +261 -0
  120. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  121. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  122. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  123. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  124. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  125. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  126. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  127. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  128. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  129. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  130. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  131. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  132. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  133. omnibase_infra/services/registry_api/routes.py +371 -0
  134. omnibase_infra/services/registry_api/service.py +837 -0
  135. omnibase_infra/services/service_capability_query.py +4 -4
  136. omnibase_infra/services/service_health.py +3 -2
  137. omnibase_infra/services/service_timeout_emitter.py +20 -3
  138. omnibase_infra/services/service_timeout_scanner.py +7 -3
  139. omnibase_infra/services/session/__init__.py +56 -0
  140. omnibase_infra/services/session/config_consumer.py +120 -0
  141. omnibase_infra/services/session/config_store.py +139 -0
  142. omnibase_infra/services/session/consumer.py +1007 -0
  143. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  144. omnibase_infra/services/session/store.py +997 -0
  145. omnibase_infra/utils/__init__.py +19 -0
  146. omnibase_infra/utils/util_atomic_file.py +261 -0
  147. omnibase_infra/utils/util_db_transaction.py +239 -0
  148. omnibase_infra/utils/util_dsn_validation.py +1 -1
  149. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  150. omnibase_infra/validation/__init__.py +3 -19
  151. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  152. omnibase_infra/validation/infra_validators.py +35 -24
  153. omnibase_infra/validation/validation_exemptions.yaml +140 -9
  154. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  155. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  156. omnibase_infra/validation/validator_security.py +473 -370
  157. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
  158. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
  159. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
  160. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
  161. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,281 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Optimistic locking retry helper for concurrent data operations.
4
+
5
+ This module provides utilities for retrying operations that may fail due to
6
+ optimistic locking conflicts. Optimistic locking is a concurrency control
7
+ strategy where conflicts are detected at write time rather than using locks.
8
+
9
+ Use Cases:
10
+ - Database UPDATE with version checks (row_count=0 indicates conflict)
11
+ - CAS (Compare-And-Swap) operations in distributed systems
12
+ - Consul KV ModifyIndex-based updates
13
+ - Any operation where conflict detection is based on return value
14
+
15
+ Design Decisions:
16
+ - No circuit breaker integration: Optimistic conflicts are application logic,
17
+ not infrastructure failures. They indicate contention, not service degradation.
18
+ - Lower initial backoff: 0.1s vs 1.0s for transient errors because conflicts
19
+ typically resolve faster as other transactions complete.
20
+ - Jitter by default: Critical for preventing thundering herd on high-contention
21
+ resources where multiple retries might synchronize.
22
+ - Caller-provided conflict check: Flexible for different conflict indicators
23
+ (row_count=0, version mismatch, boolean flags, etc.)
24
+
25
+ Example:
26
+ >>> import asyncio
27
+ >>> from omnibase_infra.utils.util_retry_optimistic import (
28
+ ... retry_on_optimistic_conflict,
29
+ ... OptimisticConflictError,
30
+ ... )
31
+ >>>
32
+ >>> attempt_count = 0
33
+ >>> async def update_with_version_check():
34
+ ... global attempt_count
35
+ ... attempt_count += 1
36
+ ... # Simulate success after 2 conflicts
37
+ ... if attempt_count < 3:
38
+ ... return {"row_count": 0} # Conflict
39
+ ... return {"row_count": 1} # Success
40
+ >>>
41
+ >>> async def main():
42
+ ... result = await retry_on_optimistic_conflict(
43
+ ... update_with_version_check,
44
+ ... check_conflict=lambda r: r["row_count"] == 0,
45
+ ... max_retries=5,
46
+ ... )
47
+ ... print(f"Success after {attempt_count} attempts")
48
+ >>>
49
+ >>> asyncio.run(main()) # doctest: +SKIP
50
+ Success after 3 attempts
51
+
52
+ See Also:
53
+ - ONEX infrastructure patterns documentation
54
+ - PostgreSQL advisory locks vs optimistic locking
55
+ - Consul KV ModifyIndex documentation
56
+
57
+ .. versionadded:: 0.10.0
58
+ Created for database operations requiring optimistic concurrency control.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ import asyncio
64
+ import logging
65
+ import random
66
+ from collections.abc import Awaitable, Callable
67
+ from typing import TYPE_CHECKING, TypeVar
68
+
69
+ if TYPE_CHECKING:
70
+ from uuid import UUID
71
+
72
+ logger = logging.getLogger(__name__)
73
+
74
+ # Generic type for the return value of the retried function
75
+ T = TypeVar("T")
76
+
77
+
78
+ class OptimisticConflictError(Exception):
79
+ """Exception raised when optimistic locking retries are exhausted.
80
+
81
+ This exception indicates that an operation failed due to repeated optimistic
82
+ locking conflicts after all retry attempts. The caller should handle this
83
+ by either:
84
+ - Reporting the conflict to the user
85
+ - Using a different conflict resolution strategy
86
+ - Applying exponential backoff at a higher level
87
+
88
+ Note:
89
+ This is a standard Python exception, not an ONEX error. Optimistic
90
+ conflicts are expected application behavior, not infrastructure failures.
91
+
92
+ Attributes:
93
+ attempts: The total number of attempts made (including initial attempt).
94
+ last_result: The result from the final failed attempt, useful for
95
+ debugging or logging the conflict state.
96
+
97
+ Example:
98
+ >>> from omnibase_infra.utils.util_retry_optimistic import OptimisticConflictError
99
+ >>>
100
+ >>> try:
101
+ ... # ... retry logic that exhausted retries
102
+ ... raise OptimisticConflictError(
103
+ ... attempts=4,
104
+ ... last_result={"row_count": 0, "current_version": 5}
105
+ ... )
106
+ ... except OptimisticConflictError as e:
107
+ ... print(f"Failed after {e.attempts} attempts")
108
+ ... print(f"Last conflict state: {e.last_result}")
109
+ Failed after 4 attempts
110
+ Last conflict state: {'row_count': 0, 'current_version': 5}
111
+ """
112
+
113
+ def __init__(self, *, attempts: int, last_result: object) -> None:
114
+ """Initialize OptimisticConflictError.
115
+
116
+ Args:
117
+ attempts: Total number of attempts made.
118
+ last_result: Result from the final attempt.
119
+ """
120
+ self.attempts = attempts
121
+ self.last_result = last_result
122
+ super().__init__(
123
+ f"Optimistic locking conflict persisted after {attempts} attempts. "
124
+ f"Last result: {last_result}"
125
+ )
126
+
127
+
128
+ async def retry_on_optimistic_conflict(
129
+ fn: Callable[[], Awaitable[T]],
130
+ *,
131
+ check_conflict: Callable[[T], bool],
132
+ max_retries: int = 3,
133
+ initial_backoff: float = 0.1,
134
+ max_backoff: float = 5.0,
135
+ backoff_multiplier: float = 2.0,
136
+ jitter: bool = True,
137
+ correlation_id: UUID | None = None,
138
+ ) -> T:
139
+ """Execute async function with retry on optimistic locking conflict.
140
+
141
+ This function implements exponential backoff retry logic for operations
142
+ that may fail due to optimistic locking conflicts. Unlike transient error
143
+ retries, this does NOT integrate with circuit breakers since conflicts
144
+ indicate contention, not service degradation.
145
+
146
+ Args:
147
+ fn: Async function to execute. Should take no arguments; use closures
148
+ or functools.partial to bind arguments. Example:
149
+ ``lambda: update_record(id=123, data=data)``
150
+ check_conflict: Callable that inspects the result and returns True if
151
+ the result indicates a conflict. Examples:
152
+ - ``lambda r: r.row_count == 0`` (database update)
153
+ - ``lambda r: r.success is False`` (boolean result)
154
+ - ``lambda r: r.version != expected_version`` (version mismatch)
155
+ max_retries: Maximum number of retry attempts after the initial attempt.
156
+ Total attempts = max_retries + 1. Defaults to 3.
157
+ initial_backoff: Initial backoff delay in seconds before first retry.
158
+ Lower than transient error defaults (0.1s vs 1.0s) because conflicts
159
+ typically resolve quickly. Defaults to 0.1.
160
+ max_backoff: Maximum backoff delay cap in seconds. Prevents excessive
161
+ wait times. Defaults to 5.0.
162
+ backoff_multiplier: Multiplier for exponential backoff between retries.
163
+ Delay doubles by default: 0.1s -> 0.2s -> 0.4s -> 0.8s. Defaults to 2.0.
164
+ jitter: If True, adds random jitter (50-150% of delay) to prevent
165
+ thundering herd when multiple clients retry simultaneously.
166
+ Strongly recommended for high-contention scenarios. Defaults to True.
167
+ correlation_id: Optional correlation ID for structured logging. When
168
+ provided, retry attempts are logged with this ID for distributed
169
+ tracing. Defaults to None.
170
+
171
+ Returns:
172
+ The result of ``fn()`` when ``check_conflict(result)`` returns False.
173
+
174
+ Raises:
175
+ OptimisticConflictError: If all retry attempts are exhausted and the
176
+ operation still indicates a conflict. Contains ``attempts`` count
177
+ and ``last_result`` for debugging.
178
+
179
+ Example:
180
+ Basic usage with row count check::
181
+
182
+ from functools import partial
183
+ from omnibase_infra.utils.util_retry_optimistic import (
184
+ retry_on_optimistic_conflict,
185
+ OptimisticConflictError,
186
+ )
187
+
188
+ async def update_with_version(id: str, data: dict, version: int):
189
+ return await db.execute(
190
+ "UPDATE t SET data=$1, version=version+1 "
191
+ "WHERE id=$2 AND version=$3",
192
+ data, id, version
193
+ )
194
+
195
+ try:
196
+ result = await retry_on_optimistic_conflict(
197
+ partial(update_with_version, "abc", {"name": "new"}, 5),
198
+ check_conflict=lambda r: r.row_count == 0,
199
+ max_retries=5,
200
+ correlation_id=correlation_id,
201
+ )
202
+ except OptimisticConflictError as e:
203
+ logger.error(f"Update failed after {e.attempts} attempts")
204
+ raise
205
+
206
+ Warning:
207
+ The ``fn`` callable should be idempotent or at least safe to retry.
208
+ This function will call ``fn`` multiple times on conflicts.
209
+
210
+ Note:
211
+ Backoff timing with default parameters (jitter disabled for clarity):
212
+ - Attempt 1: immediate
213
+ - Attempt 2: wait 0.1s
214
+ - Attempt 3: wait 0.2s
215
+ - Attempt 4: wait 0.4s
216
+ - Total max wait: ~0.7s (with max_retries=3)
217
+
218
+ .. versionadded:: 0.10.0
219
+ """
220
+ backoff = initial_backoff
221
+ last_result: T | None = None
222
+
223
+ for attempt in range(max_retries + 1): # +1 for initial attempt
224
+ result = await fn()
225
+
226
+ if not check_conflict(result):
227
+ # Success - no conflict detected
228
+ if attempt > 0 and correlation_id is not None:
229
+ logger.info(
230
+ "Optimistic conflict resolved after %d retries",
231
+ attempt,
232
+ extra={
233
+ "correlation_id": str(correlation_id),
234
+ "total_attempts": attempt + 1,
235
+ "action": "optimistic_conflict_resolved",
236
+ },
237
+ )
238
+ return result
239
+
240
+ # Conflict detected - store result for potential error reporting
241
+ last_result = result
242
+
243
+ if attempt == max_retries:
244
+ # All retries exhausted
245
+ break
246
+
247
+ # Log retry attempt if correlation_id provided
248
+ if correlation_id is not None:
249
+ logger.debug(
250
+ "Optimistic conflict detected, retrying (attempt %d/%d)",
251
+ attempt + 1,
252
+ max_retries + 1,
253
+ extra={
254
+ "correlation_id": str(correlation_id),
255
+ "attempt": attempt + 1,
256
+ "max_attempts": max_retries + 1,
257
+ "backoff_seconds": backoff,
258
+ "action": "optimistic_conflict_retry",
259
+ },
260
+ )
261
+
262
+ # Calculate delay with optional jitter
263
+ delay = min(backoff, max_backoff)
264
+ if jitter:
265
+ # Apply 50-150% jitter to prevent thundering herd
266
+ delay *= 0.5 + random.random()
267
+
268
+ await asyncio.sleep(delay)
269
+ backoff *= backoff_multiplier
270
+
271
+ # All retries exhausted - raise conflict error
272
+ # Defensive check - last_result is guaranteed to be set after at least one attempt
273
+ if last_result is None:
274
+ raise AssertionError("Unreachable: last_result must be set after retries")
275
+ raise OptimisticConflictError(attempts=max_retries + 1, last_result=last_result)
276
+
277
+
278
+ __all__: list[str] = [
279
+ "OptimisticConflictError",
280
+ "retry_on_optimistic_conflict",
281
+ ]
@@ -200,17 +200,8 @@ from omnibase_infra.validation.validator_runtime_shape import (
200
200
  enforce_execution_shape,
201
201
  )
202
202
 
203
- # Security validation for handler introspection and security constraints
204
- from omnibase_infra.validation.validator_security import (
205
- SENSITIVE_METHOD_PATTERNS,
206
- SENSITIVE_PARAMETER_NAMES,
207
- SecurityRuleId,
208
- convert_to_validation_error,
209
- has_sensitive_parameters,
210
- is_sensitive_method_name,
211
- validate_handler_security,
212
- validate_method_exposure,
213
- )
203
+ # Security validation (OMN-1277) - contract-driven validator
204
+ from omnibase_infra.validation.validator_security import ValidatorSecurity
214
205
 
215
206
  # Topic category validation for execution shape enforcement
216
207
  from omnibase_infra.validation.validator_topic_category import (
@@ -229,8 +220,6 @@ __all__: list[str] = [
229
220
  "EXECUTION_SHAPE_RULES", # Runtime shape validation rules
230
221
  "INFRA_MAX_UNIONS", # Infrastructure max union threshold
231
222
  "NODE_ARCHETYPE_EXPECTED_CATEGORIES", # Node archetype categories
232
- "SENSITIVE_METHOD_PATTERNS", # Security validation patterns
233
- "SENSITIVE_PARAMETER_NAMES", # Security validation names
234
223
  "TOPIC_CATEGORY_PATTERNS", # Topic category patterns
235
224
  "TOPIC_SUFFIXES", # Topic suffix constants
236
225
  # Errors
@@ -239,7 +228,6 @@ __all__: list[str] = [
239
228
  "RoutingCoverageError", # Routing coverage error (OMN-958)
240
229
  # Enums
241
230
  "EnumContractViolationSeverity", # Contract violation severity
242
- "SecurityRuleId", # Security rule identifiers
243
231
  # Models
244
232
  "ModelAnyTypeValidationResult", # Any type validation result (OMN-1276)
245
233
  "ModelContractLintResult", # Contract lint result
@@ -261,9 +249,9 @@ __all__: list[str] = [
261
249
  "TopicCategoryASTVisitor", # Topic category AST visitor
262
250
  "TopicCategoryValidator", # Topic category validator
263
251
  "ValidationAggregator", # Validation error aggregation (OMN-1091)
252
+ "ValidatorSecurity", # Contract-driven security validator (OMN-1277)
264
253
  # Functions
265
254
  "check_routing_coverage_ci", # CI routing coverage check
266
- "convert_to_validation_error", # Error conversion utility
267
255
  "detect_message_category", # Message category detection
268
256
  "discover_message_types", # Message type discovery
269
257
  "discover_registered_routes", # Route discovery
@@ -271,9 +259,7 @@ __all__: list[str] = [
271
259
  "enforce_execution_shape", # Execution shape enforcement
272
260
  "get_execution_shape_rules", # Get shape rules
273
261
  "get_validation_summary", # Get validation summary
274
- "has_sensitive_parameters", # Sensitive parameter check
275
262
  "is_isinstance_union", # Check if union is in isinstance() call
276
- "is_sensitive_method_name", # Sensitive method check
277
263
  "lint_contract_file", # Lint single contract file
278
264
  "lint_contracts_ci", # CI contract linting
279
265
  "lint_contracts_in_directory", # Directory contract linting
@@ -286,7 +272,6 @@ __all__: list[str] = [
286
272
  "validate_execution_shapes", # Execution shape validation
287
273
  "validate_execution_shapes_ci", # CI shape validation
288
274
  "validate_handler_registration", # Handler registration validation (OMN-1098)
289
- "validate_handler_security", # Handler security validation
290
275
  "validate_infra_all", # Infrastructure validation
291
276
  "validate_infra_architecture", # Infrastructure architecture
292
277
  "validate_infra_circular_imports", # Circular import check
@@ -298,7 +283,6 @@ __all__: list[str] = [
298
283
  "validate_localhandler_in_file", # LocalHandler file validation (OMN-743)
299
284
  "validate_message_chain", # Message chain validation
300
285
  "validate_message_on_topic", # Topic message validation
301
- "validate_method_exposure", # Method exposure validation
302
286
  "validate_patterns", # Re-export from omnibase_core
303
287
  "validate_routing_coverage_on_startup", # Startup routing check
304
288
  "validate_topic_categories_in_directory", # Directory topic validation
@@ -0,0 +1,114 @@
1
+ contract_kind: validation_subcontract
2
+ validation:
3
+ version:
4
+ major: 1
5
+ minor: 0
6
+ patch: 0
7
+ validator_id: security
8
+ validator_name: ONEX Security Validator
9
+ validator_description: |
10
+ Validates Python source files for security concerns in handler implementations.
11
+ Detects sensitive method names that should be private, credential exposure in
12
+ method signatures, and admin/internal operations exposed publicly.
13
+
14
+ This validator performs static AST analysis to identify:
15
+ - Public methods with sensitive names (get_password, get_secret, etc.)
16
+ - Method signatures containing sensitive parameter names
17
+ - Admin/internal methods exposed without underscore prefix
18
+ - Decrypt operations exposed publicly
19
+
20
+ Part of OMN-1277: Refactor validators to be Handler and contract-driven.
21
+ target_patterns:
22
+ - "**/*.py"
23
+ exclude_patterns:
24
+ - "**/node_modules/**"
25
+ - "**/.git/**"
26
+ - "**/venv/**"
27
+ - "**/.venv/**"
28
+ - "**/__pycache__/**"
29
+ - "**/archived/**"
30
+ - "**/tests/**"
31
+ - "**/*_test.py"
32
+ - "**/test_*.py"
33
+ - "**/conftest.py"
34
+ rules:
35
+ - rule_id: sensitive_method_exposed
36
+ description: |
37
+ Detects public methods with sensitive names that should be private.
38
+ Matches patterns: get_password, get_secret, get_token, get_api_key,
39
+ get_credential*, fetch_password, fetch_secret, fetch_token,
40
+ validate_password, check_password, verify_password.
41
+ severity: error
42
+ enabled: true
43
+ parameters:
44
+ patterns:
45
+ - "^get_password$"
46
+ - "^get_secret$"
47
+ - "^get_token$"
48
+ - "^get_api_key$"
49
+ - "^get_credential"
50
+ - "^fetch_password$"
51
+ - "^fetch_secret$"
52
+ - "^fetch_token$"
53
+ - "^validate_password$"
54
+ - "^check_password$"
55
+ - "^verify_password$"
56
+ - rule_id: credential_in_signature
57
+ description: |
58
+ Detects method signatures containing sensitive parameter names.
59
+ Matches parameters: password, secret, token, api_key, apikey,
60
+ access_key, private_key, credential, auth_token, bearer_token,
61
+ decrypt_key, encryption_key.
62
+ severity: error
63
+ enabled: true
64
+ parameters:
65
+ sensitive_params:
66
+ - "password"
67
+ - "secret"
68
+ - "token"
69
+ - "api_key"
70
+ - "apikey"
71
+ - "access_key"
72
+ - "private_key"
73
+ - "credential"
74
+ - "auth_token"
75
+ - "bearer_token"
76
+ - "decrypt_key"
77
+ - "encryption_key"
78
+ - rule_id: admin_method_public
79
+ description: |
80
+ Detects admin or internal methods exposed without underscore prefix.
81
+ Matches patterns: admin_*, internal_*.
82
+ severity: warning
83
+ enabled: true
84
+ parameters:
85
+ patterns:
86
+ - "^admin_"
87
+ - "^internal_"
88
+ - rule_id: decrypt_method_public
89
+ description: |
90
+ Detects decrypt operations exposed publicly.
91
+ Matches patterns: decrypt_*.
92
+ severity: warning
93
+ enabled: true
94
+ parameters:
95
+ patterns:
96
+ - "^decrypt_"
97
+ # ============================================================================
98
+ # LINE-BASED SUPPRESSION
99
+ # ============================================================================
100
+ # If ANY pattern below appears on a source line, ALL security violations
101
+ # on that line are suppressed.
102
+ #
103
+ # Usage example:
104
+ # def get_password(self) -> str: # ONEX_EXCLUDE: security - required for legacy API
105
+ # def admin_reset(self) -> None: # security-ok: admin endpoint with auth guard
106
+ # ============================================================================
107
+ suppression_comments:
108
+ - "# ONEX_EXCLUDE: security"
109
+ - "# security-ok:"
110
+ severity_default: error
111
+ fail_on_error: true
112
+ fail_on_warning: false
113
+ max_violations: 0
114
+ parallel_execution: true
@@ -37,10 +37,12 @@ from typing import TypedDict
37
37
  import yaml
38
38
 
39
39
  from omnibase_core.models.common import ModelValidationMetadata
40
+ from omnibase_core.models.primitives import ModelSemVer
40
41
  from omnibase_core.models.validation.model_union_pattern import ModelUnionPattern
41
42
  from omnibase_core.validation import (
42
43
  CircularImportValidator,
43
44
  ModelContractValidationResult,
45
+ ModelImportValidationResult,
44
46
  ModelModuleImportResult,
45
47
  ModelValidationResult,
46
48
  validate_architecture,
@@ -423,7 +425,11 @@ INFRA_NODES_PATH = "src/omnibase_infra/nodes/"
423
425
  # (+2 unions for EnumPolicyType | str in validate_policy_type_value)
424
426
  # (-1 union: fix PolicyTypeInput validator coercion, changed return
425
427
  # type from str | EnumPolicyType to EnumPolicyType)
426
- INFRA_MAX_UNIONS = 96
428
+ # - 98 (2026-01-20): OMN-1277 security validator contract refactoring (+2 unions)
429
+ # ast.FunctionDef | ast.AsyncFunctionDef for AST method type checking
430
+ # - 105 (2026-01-21): Contract-driven handler config loading (+4 unions)
431
+ # ModelHandlerContract transport config fields and lifecycle types
432
+ INFRA_MAX_UNIONS = 105
427
433
 
428
434
  # Maximum allowed architecture violations in infrastructure code.
429
435
  # Set to 0 (strict enforcement) to ensure one-model-per-file principle is always followed.
@@ -747,13 +753,14 @@ def validate_infra_contract_deep(
747
753
  return result
748
754
 
749
755
  # If result is a different type, wrap it in ModelContractValidationResult
750
- # Default to passed=False for unknown result types to avoid silently masking failures
751
- # Check 'passed' first, then 'is_valid' as fallback (some validators use is_valid)
756
+ # Default to is_valid=False for unknown result types to avoid silently masking failures
757
+ # Check 'is_valid' first, then 'passed' as fallback (some validators use passed)
752
758
  return ModelContractValidationResult(
753
- passed=getattr(result, "passed", getattr(result, "is_valid", False)),
759
+ is_valid=getattr(result, "is_valid", getattr(result, "passed", False)),
754
760
  score=getattr(result, "score", 0.0),
755
- errors=getattr(result, "errors", []),
761
+ violations=getattr(result, "violations", getattr(result, "errors", [])),
756
762
  warnings=getattr(result, "warnings", []),
763
+ interface_version=ModelSemVer(major=1, minor=0, patch=0),
757
764
  )
758
765
 
759
766
 
@@ -1318,29 +1325,33 @@ def validate_infra_union_usage(
1318
1325
  # Note: ModelValidationMetadata uses extra="allow", so extension fields
1319
1326
  # are accepted as int values.
1320
1327
  # See docstring "Metadata Extension Fields" section for field documentation.
1328
+ #
1329
+ # Extension fields are passed via model_construct() to satisfy type checker
1330
+ # while preserving runtime behavior with extra="allow".
1331
+ metadata_fields: dict[str, object] = {
1332
+ # Standard ModelValidationMetadata fields (formally defined)
1333
+ "validation_type": "union_usage",
1334
+ "files_processed": files_processed,
1335
+ "violations_found": len(filtered_issues),
1336
+ "total_unions": total_count, # Base field: all unions found
1337
+ "max_unions": max_unions, # Base field: configured threshold
1338
+ "strict_mode": strict, # Base field: whether strict mode enabled
1339
+ # Extension fields (via extra="allow", typed as int)
1340
+ # These provide transparency into the exclusion logic:
1341
+ "non_optional_unions": threshold_count, # What threshold actually checks
1342
+ "optional_unions_excluded": optional_count, # X | None patterns
1343
+ "isinstance_unions_excluded": isinstance_count, # isinstance(x, A | B) patterns
1344
+ }
1321
1345
  return ModelValidationResult(
1322
1346
  is_valid=is_valid,
1323
1347
  errors=filtered_issues,
1324
- metadata=ModelValidationMetadata(
1325
- # Standard ModelValidationMetadata fields (formally defined)
1326
- validation_type="union_usage",
1327
- files_processed=files_processed,
1328
- violations_found=len(filtered_issues),
1329
- total_unions=total_count, # Base field: all unions found
1330
- max_unions=max_unions, # Base field: configured threshold
1331
- strict_mode=strict, # Base field: whether strict mode enabled
1332
- # Extension fields (via extra="allow", typed as int)
1333
- # These provide transparency into the exclusion logic:
1334
- non_optional_unions=threshold_count, # What threshold actually checks
1335
- optional_unions_excluded=optional_count, # X | None patterns
1336
- isinstance_unions_excluded=isinstance_count, # isinstance(x, A | B) patterns
1337
- ),
1348
+ metadata=ModelValidationMetadata.model_construct(**metadata_fields), # type: ignore[arg-type]
1338
1349
  )
1339
1350
 
1340
1351
 
1341
1352
  def validate_infra_circular_imports(
1342
1353
  directory: PathInput = INFRA_SRC_PATH,
1343
- ) -> ModelModuleImportResult:
1354
+ ) -> ModelImportValidationResult:
1344
1355
  """
1345
1356
  Check for circular imports in infrastructure code.
1346
1357
 
@@ -1351,7 +1362,7 @@ def validate_infra_circular_imports(
1351
1362
  directory: Directory to check. Defaults to infrastructure source.
1352
1363
 
1353
1364
  Returns:
1354
- ModelModuleImportResult with detailed import validation results.
1365
+ ModelImportValidationResult with detailed import validation results.
1355
1366
  Use result.has_circular_imports to check for issues.
1356
1367
  """
1357
1368
  validator = CircularImportValidator(source_path=Path(directory))
@@ -1361,7 +1372,7 @@ def validate_infra_circular_imports(
1361
1372
  def validate_infra_all(
1362
1373
  directory: PathInput = INFRA_SRC_PATH,
1363
1374
  nodes_directory: PathInput = INFRA_NODES_PATH,
1364
- ) -> dict[str, ValidationResult | ModelModuleImportResult]:
1375
+ ) -> dict[str, ValidationResult | ModelImportValidationResult]:
1365
1376
  """
1366
1377
  Run all validations on infrastructure code.
1367
1378
 
@@ -1379,7 +1390,7 @@ def validate_infra_all(
1379
1390
  Returns:
1380
1391
  Dictionary mapping validator name to result.
1381
1392
  """
1382
- results: dict[str, ValidationResult | ModelModuleImportResult] = {}
1393
+ results: dict[str, ValidationResult | ModelImportValidationResult] = {}
1383
1394
 
1384
1395
  # HIGH priority validators
1385
1396
  results["architecture"] = validate_infra_architecture(directory)
@@ -1394,7 +1405,7 @@ def validate_infra_all(
1394
1405
 
1395
1406
 
1396
1407
  def get_validation_summary(
1397
- results: dict[str, ValidationResult | ModelModuleImportResult],
1408
+ results: dict[str, ValidationResult | ModelImportValidationResult],
1398
1409
  ) -> dict[str, int | list[str]]:
1399
1410
  """
1400
1411
  Generate a summary of validation results.