omnibase_infra 0.3.2__py3-none-any.whl → 0.4.0__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 (57) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/errors/__init__.py +4 -0
  3. omnibase_infra/errors/error_infra.py +60 -0
  4. omnibase_infra/handlers/__init__.py +3 -0
  5. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  6. omnibase_infra/handlers/models/__init__.py +14 -0
  7. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  8. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  9. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  10. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  11. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  12. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  13. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  14. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  15. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  16. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  17. omnibase_infra/models/runtime/__init__.py +4 -0
  18. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  19. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  20. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  21. omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
  23. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  24. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  25. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  26. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  27. omnibase_infra/runtime/__init__.py +7 -0
  28. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  29. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  30. omnibase_infra/runtime/contract_registration_event_router.py +5 -5
  31. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  32. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  33. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  34. omnibase_infra/runtime/registry_policy.py +29 -15
  35. omnibase_infra/runtime/request_response_wiring.py +15 -7
  36. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  37. omnibase_infra/runtime/util_version.py +5 -1
  38. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  39. omnibase_infra/services/contract_publisher/config.py +4 -4
  40. omnibase_infra/services/contract_publisher/service.py +8 -5
  41. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  42. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  43. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  44. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  45. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  46. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  47. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  48. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  49. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  50. omnibase_infra/utils/__init__.py +7 -0
  51. omnibase_infra/utils/util_db_error_context.py +292 -0
  52. omnibase_infra/validation/validation_exemptions.yaml +11 -0
  53. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
  54. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
  55. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  56. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  57. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -76,7 +76,7 @@ See Also
76
76
  - Runtime kernel: omnibase_infra.runtime.service_kernel
77
77
  """
78
78
 
79
- __version__ = "0.3.2"
79
+ __version__ = "0.4.0"
80
80
 
81
81
  from . import (
82
82
  enums,
@@ -10,6 +10,7 @@ Exports:
10
10
  ModelInfraErrorContext: Configuration model for bundled error context
11
11
  RuntimeHostError: Base infrastructure error class
12
12
  ProtocolConfigurationError: Protocol configuration validation errors
13
+ ProtocolDependencyResolutionError: Protocol dependency resolution errors
13
14
  SecretResolutionError: Secret/credential resolution errors
14
15
  InfraConnectionError: Infrastructure connection errors
15
16
  InfraTimeoutError: Infrastructure timeout errors
@@ -115,6 +116,7 @@ from omnibase_infra.errors.error_infra import (
115
116
  InfraTimeoutError,
116
117
  InfraUnavailableError,
117
118
  ProtocolConfigurationError,
119
+ ProtocolDependencyResolutionError,
118
120
  RuntimeHostError,
119
121
  SecretResolutionError,
120
122
  UnknownHandlerTypeError,
@@ -162,6 +164,8 @@ __all__: list[str] = [
162
164
  "ModelTimeoutErrorContext",
163
165
  "PolicyRegistryError",
164
166
  "ProtocolConfigurationError",
167
+ # Protocol dependency resolution errors
168
+ "ProtocolDependencyResolutionError",
165
169
  # Repository errors
166
170
  "RepositoryContractError",
167
171
  "RepositoryError",
@@ -597,6 +597,65 @@ class UnknownHandlerTypeError(RuntimeHostError):
597
597
  )
598
598
 
599
599
 
600
+ class ProtocolDependencyResolutionError(RuntimeHostError):
601
+ """Raised when protocol dependencies cannot be resolved from container.
602
+
603
+ Used when a node's contract.yaml declares protocol dependencies that
604
+ are not registered in the container's service_registry. This is a
605
+ fail-fast error that prevents node creation with missing dependencies.
606
+
607
+ Example:
608
+ >>> context = ModelInfraErrorContext(
609
+ ... transport_type=EnumInfraTransportType.RUNTIME,
610
+ ... operation="resolve_dependencies",
611
+ ... target_name="NodeContractPersistenceEffect",
612
+ ... )
613
+ >>> raise ProtocolDependencyResolutionError(
614
+ ... "Missing required protocols for node",
615
+ ... context=context,
616
+ ... missing_protocols=["ProtocolPostgresAdapter"],
617
+ ... node_name="node_contract_persistence_effect",
618
+ ... )
619
+
620
+ .. versionadded:: 0.x.x
621
+ Part of OMN-1732 runtime dependency injection.
622
+ """
623
+
624
+ def __init__(
625
+ self,
626
+ message: str,
627
+ context: ModelInfraErrorContext | None = None,
628
+ *,
629
+ missing_protocols: list[str] | None = None,
630
+ node_name: str | None = None,
631
+ contract_path: str | None = None,
632
+ **extra_context: object,
633
+ ) -> None:
634
+ """Initialize ProtocolDependencyResolutionError.
635
+
636
+ Args:
637
+ message: Human-readable error message
638
+ context: Bundled infrastructure context
639
+ missing_protocols: List of protocol class names that could not be resolved
640
+ node_name: Name of the node requiring the protocols
641
+ contract_path: Path to the contract.yaml file (for debugging)
642
+ **extra_context: Additional context information
643
+ """
644
+ if missing_protocols is not None:
645
+ extra_context["missing_protocols"] = missing_protocols
646
+ if node_name is not None:
647
+ extra_context["node_name"] = node_name
648
+ if contract_path is not None:
649
+ extra_context["contract_path"] = contract_path
650
+
651
+ super().__init__(
652
+ message=message,
653
+ error_code=EnumCoreErrorCode.DEPENDENCY_ERROR,
654
+ context=context,
655
+ **extra_context,
656
+ )
657
+
658
+
600
659
  __all__: list[str] = [
601
660
  "EnvelopeValidationError",
602
661
  "InfraAuthenticationError",
@@ -604,6 +663,7 @@ __all__: list[str] = [
604
663
  "InfraTimeoutError",
605
664
  "InfraUnavailableError",
606
665
  "ProtocolConfigurationError",
666
+ "ProtocolDependencyResolutionError",
607
667
  "RuntimeHostError",
608
668
  "SecretResolutionError",
609
669
  "UnknownHandlerTypeError",
@@ -20,6 +20,7 @@ Available Handlers:
20
20
  - HandlerGraph: Graph database handler (Memgraph/Neo4j via Bolt protocol)
21
21
  - HandlerIntent: Intent storage and query handler wrapping HandlerGraph (demo wiring)
22
22
  - HandlerQdrant: Qdrant vector database handler (MVP: create, upsert, search, delete)
23
+ - HandlerSlackWebhook: Slack webhook handler for infrastructure alerting
23
24
 
24
25
  Response Models:
25
26
  - ModelDbQueryPayload: Database query result payload
@@ -45,6 +46,7 @@ from omnibase_infra.handlers.handler_manifest_persistence import (
45
46
  )
46
47
  from omnibase_infra.handlers.handler_mcp import HandlerMCP
47
48
  from omnibase_infra.handlers.handler_qdrant import HandlerQdrant
49
+ from omnibase_infra.handlers.handler_slack_webhook import HandlerSlackWebhook
48
50
  from omnibase_infra.handlers.handler_vault import HandlerVault
49
51
  from omnibase_infra.handlers.models import (
50
52
  ModelConsulHandlerPayload,
@@ -71,6 +73,7 @@ __all__: list[str] = [
71
73
  "HandlerManifestPersistence",
72
74
  "HandlerMCP",
73
75
  "HandlerQdrant",
76
+ "HandlerSlackWebhook",
74
77
  "HandlerVault",
75
78
  "ModelConsulHandlerPayload",
76
79
  "ModelConsulHandlerResponse",
@@ -0,0 +1,426 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Webhook Handler - Infrastructure alerting via Slack webhooks.
4
+
5
+ This handler sends alerts to Slack channels using incoming webhooks,
6
+ with support for Block Kit formatting, retry with exponential backoff,
7
+ and rate limit handling.
8
+
9
+ Architecture:
10
+ This handler follows the ONEX operation handler pattern:
11
+ - Receives typed input (ModelSlackAlert)
12
+ - Executes a single responsibility (Slack webhook delivery)
13
+ - Returns typed output (ModelSlackAlertResult)
14
+ - Uses error sanitization for security
15
+ - Stateless and coroutine-safe for concurrent calls
16
+
17
+ Handler Responsibilities:
18
+ - Format alerts as Slack Block Kit messages
19
+ - Send webhooks with configurable retry logic
20
+ - Handle 429 rate limiting gracefully
21
+ - Sanitize errors to prevent credential exposure
22
+ - Track operation timing and retry counts
23
+
24
+ Configuration:
25
+ The webhook URL is resolved from the SLACK_WEBHOOK_URL environment
26
+ variable. This keeps credentials out of code and configuration files.
27
+
28
+ Coroutine Safety:
29
+ This handler is stateless and coroutine-safe for concurrent calls
30
+ with different request instances.
31
+
32
+ Related Tickets:
33
+ - OMN-1905: Add declarative Slack webhook handler to omnibase_infra
34
+ - OMN-1895: Wiring Health Monitor alerting (blocked by this)
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import asyncio
40
+ import logging
41
+ import os
42
+ import time
43
+ from typing import TYPE_CHECKING
44
+
45
+ import aiohttp
46
+
47
+ from omnibase_infra.enums import EnumInfraTransportType
48
+ from omnibase_infra.errors import (
49
+ InfraConnectionError,
50
+ InfraTimeoutError,
51
+ InfraUnavailableError,
52
+ ModelInfraErrorContext,
53
+ ProtocolConfigurationError,
54
+ )
55
+ from omnibase_infra.handlers.models.model_slack_alert import (
56
+ EnumAlertSeverity,
57
+ ModelSlackAlert,
58
+ ModelSlackAlertResult,
59
+ )
60
+ from omnibase_infra.utils import sanitize_error_message
61
+
62
+ if TYPE_CHECKING:
63
+ from uuid import UUID
64
+
65
+ logger = logging.getLogger(__name__)
66
+
67
+ # Default retry configuration
68
+ _DEFAULT_MAX_RETRIES: int = 3
69
+ _DEFAULT_RETRY_BACKOFF_SECONDS: tuple[float, ...] = (1.0, 2.0, 4.0)
70
+ _DEFAULT_TIMEOUT_SECONDS: float = 10.0
71
+
72
+ # Slack Block Kit emoji mapping for severity levels
73
+ _SEVERITY_EMOJI: dict[EnumAlertSeverity, str] = {
74
+ EnumAlertSeverity.CRITICAL: ":red_circle:",
75
+ EnumAlertSeverity.ERROR: ":red_circle:",
76
+ EnumAlertSeverity.WARNING: ":large_yellow_circle:",
77
+ EnumAlertSeverity.INFO: ":large_blue_circle:",
78
+ }
79
+
80
+ # Default titles for each severity level
81
+ _SEVERITY_TITLES: dict[EnumAlertSeverity, str] = {
82
+ EnumAlertSeverity.CRITICAL: "Critical Alert",
83
+ EnumAlertSeverity.ERROR: "Error Alert",
84
+ EnumAlertSeverity.WARNING: "Warning",
85
+ EnumAlertSeverity.INFO: "Info",
86
+ }
87
+
88
+
89
+ class HandlerSlackWebhook:
90
+ """Handler for Slack webhook alert delivery.
91
+
92
+ Encapsulates all Slack-specific alerting logic for declarative
93
+ node compliance. Supports Block Kit formatting, retry with exponential
94
+ backoff, and rate limit handling.
95
+
96
+ Error Handling:
97
+ All errors are sanitized before inclusion in the result to prevent
98
+ credential exposure. The handler never raises exceptions during
99
+ normal operation - errors are captured in ModelSlackAlertResult.
100
+
101
+ Rate Limiting:
102
+ HTTP 429 responses trigger automatic retry with backoff. After
103
+ max retries are exhausted, the operation fails gracefully with
104
+ an error result rather than raising an exception.
105
+
106
+ Attributes:
107
+ _webhook_url: Slack webhook URL (from env or constructor)
108
+ _http_session: Optional shared aiohttp session
109
+ _max_retries: Maximum retry attempts for failed requests
110
+ _retry_backoff: Tuple of backoff delays in seconds
111
+ _timeout: HTTP request timeout in seconds
112
+
113
+ Example:
114
+ >>> import asyncio
115
+ >>> handler = HandlerSlackWebhook()
116
+ >>> alert = ModelSlackAlert(
117
+ ... severity=EnumAlertSeverity.ERROR,
118
+ ... message="Circuit breaker opened for Consul",
119
+ ... title="Infrastructure Alert",
120
+ ... )
121
+ >>> # result = await handler.handle(alert)
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ webhook_url: str | None = None,
127
+ http_session: aiohttp.ClientSession | None = None,
128
+ max_retries: int = _DEFAULT_MAX_RETRIES,
129
+ retry_backoff: tuple[float, ...] = _DEFAULT_RETRY_BACKOFF_SECONDS,
130
+ timeout: float = _DEFAULT_TIMEOUT_SECONDS,
131
+ ) -> None:
132
+ """Initialize handler with webhook URL and optional HTTP session.
133
+
134
+ Args:
135
+ webhook_url: Slack webhook URL. If not provided, reads from
136
+ SLACK_WEBHOOK_URL environment variable.
137
+ http_session: Optional shared aiohttp ClientSession. If not
138
+ provided, a new session is created per request.
139
+ max_retries: Maximum retry attempts for failed requests.
140
+ Default is 3.
141
+ retry_backoff: Tuple of backoff delays in seconds for each
142
+ retry attempt. Default is (1.0, 2.0, 4.0).
143
+ timeout: HTTP request timeout in seconds. Default is 10.0.
144
+ """
145
+ self._webhook_url: str = (
146
+ webhook_url if webhook_url else os.getenv("SLACK_WEBHOOK_URL", "")
147
+ )
148
+ self._http_session = http_session
149
+ self._max_retries = max_retries
150
+ self._retry_backoff = retry_backoff
151
+ self._timeout = timeout
152
+
153
+ async def handle(
154
+ self,
155
+ alert: ModelSlackAlert,
156
+ ) -> ModelSlackAlertResult:
157
+ """Execute Slack webhook alert delivery.
158
+
159
+ Formats the alert as a Slack Block Kit message and sends it
160
+ to the configured webhook URL with retry logic.
161
+
162
+ Args:
163
+ alert: Alert payload containing severity, message, and optional
164
+ details.
165
+
166
+ Returns:
167
+ ModelSlackAlertResult with:
168
+ - success: True if alert was delivered
169
+ - duration_ms: Time taken for the operation
170
+ - correlation_id: From the input alert
171
+ - error: Sanitized error message (only on failure)
172
+ - error_code: Error code for programmatic handling
173
+ - retry_count: Number of retry attempts made
174
+
175
+ Note:
176
+ This handler does not raise exceptions during normal operation.
177
+ All errors are captured and returned in ModelSlackAlertResult
178
+ to support graceful degradation in alerting scenarios.
179
+ """
180
+ start_time = time.perf_counter()
181
+ correlation_id = alert.correlation_id
182
+ retry_count = 0
183
+
184
+ # Validate webhook URL is configured
185
+ if not self._webhook_url:
186
+ duration_ms = (time.perf_counter() - start_time) * 1000
187
+ return ModelSlackAlertResult(
188
+ success=False,
189
+ duration_ms=duration_ms,
190
+ correlation_id=correlation_id,
191
+ error="SLACK_WEBHOOK_URL not configured",
192
+ error_code="SLACK_NOT_CONFIGURED",
193
+ retry_count=0,
194
+ )
195
+
196
+ # Format the alert as Block Kit message
197
+ slack_payload = self._format_block_kit_message(alert)
198
+
199
+ # Create or use existing HTTP session
200
+ session_created = False
201
+ session = self._http_session
202
+ if session is None:
203
+ session = aiohttp.ClientSession()
204
+ session_created = True
205
+
206
+ try:
207
+ return await self._send_with_retry(
208
+ session=session,
209
+ slack_payload=slack_payload,
210
+ correlation_id=correlation_id,
211
+ start_time=start_time,
212
+ )
213
+ finally:
214
+ # Close session if we created it
215
+ if session_created and session is not None:
216
+ await session.close()
217
+
218
+ async def _send_with_retry(
219
+ self,
220
+ session: aiohttp.ClientSession,
221
+ slack_payload: dict[str, object],
222
+ correlation_id: UUID,
223
+ start_time: float,
224
+ ) -> ModelSlackAlertResult:
225
+ """Send webhook with retry logic and rate limit handling.
226
+
227
+ Args:
228
+ session: aiohttp ClientSession for HTTP requests
229
+ slack_payload: Formatted Slack Block Kit payload
230
+ correlation_id: UUID for distributed tracing
231
+ start_time: Performance timer start for duration calculation
232
+
233
+ Returns:
234
+ ModelSlackAlertResult with operation outcome
235
+ """
236
+ retry_count = 0
237
+ last_error: str | None = None
238
+ last_error_code: str | None = None
239
+
240
+ for attempt in range(self._max_retries + 1):
241
+ try:
242
+ async with session.post(
243
+ self._webhook_url,
244
+ json=slack_payload,
245
+ timeout=aiohttp.ClientTimeout(total=self._timeout),
246
+ ) as response:
247
+ duration_ms = (time.perf_counter() - start_time) * 1000
248
+
249
+ if response.status == 200:
250
+ # Success
251
+ logger.info(
252
+ "Slack alert delivered successfully",
253
+ extra={
254
+ "correlation_id": str(correlation_id),
255
+ "duration_ms": round(duration_ms, 2),
256
+ "retry_count": retry_count,
257
+ },
258
+ )
259
+ return ModelSlackAlertResult(
260
+ success=True,
261
+ duration_ms=duration_ms,
262
+ correlation_id=correlation_id,
263
+ retry_count=retry_count,
264
+ )
265
+
266
+ elif response.status == 429:
267
+ # Rate limited - retry with backoff
268
+ last_error = "Slack rate limit (429)"
269
+ last_error_code = "SLACK_RATE_LIMITED"
270
+ logger.warning(
271
+ "Slack rate limited, will retry",
272
+ extra={
273
+ "correlation_id": str(correlation_id),
274
+ "attempt": attempt + 1,
275
+ "max_attempts": self._max_retries + 1,
276
+ },
277
+ )
278
+
279
+ elif response.status >= 400:
280
+ # Other HTTP error
281
+ response_text = await response.text()
282
+ last_error = f"HTTP {response.status}: {response_text[:100]}"
283
+ last_error_code = f"SLACK_HTTP_{response.status}"
284
+ logger.warning(
285
+ "Slack webhook error",
286
+ extra={
287
+ "correlation_id": str(correlation_id),
288
+ "status_code": response.status,
289
+ "attempt": attempt + 1,
290
+ },
291
+ )
292
+
293
+ except TimeoutError:
294
+ last_error = "Request timeout"
295
+ last_error_code = "SLACK_TIMEOUT"
296
+ logger.warning(
297
+ "Slack webhook timeout",
298
+ extra={
299
+ "correlation_id": str(correlation_id),
300
+ "timeout_seconds": self._timeout,
301
+ "attempt": attempt + 1,
302
+ },
303
+ )
304
+
305
+ except aiohttp.ClientConnectorError as e:
306
+ last_error = sanitize_error_message(e)
307
+ last_error_code = "SLACK_CONNECTION_ERROR"
308
+ logger.warning(
309
+ "Slack webhook connection error",
310
+ extra={
311
+ "correlation_id": str(correlation_id),
312
+ "attempt": attempt + 1,
313
+ },
314
+ )
315
+
316
+ except aiohttp.ClientError as e:
317
+ last_error = sanitize_error_message(e)
318
+ last_error_code = "SLACK_CLIENT_ERROR"
319
+ logger.warning(
320
+ "Slack webhook client error",
321
+ extra={
322
+ "correlation_id": str(correlation_id),
323
+ "attempt": attempt + 1,
324
+ "error_type": type(e).__name__,
325
+ },
326
+ )
327
+
328
+ # Retry with backoff if we have retries remaining
329
+ if attempt < self._max_retries:
330
+ backoff_index = min(attempt, len(self._retry_backoff) - 1)
331
+ backoff_seconds = self._retry_backoff[backoff_index]
332
+ logger.info(
333
+ "Retrying Slack webhook",
334
+ extra={
335
+ "correlation_id": str(correlation_id),
336
+ "backoff_seconds": backoff_seconds,
337
+ "attempt": attempt + 1,
338
+ },
339
+ )
340
+ await asyncio.sleep(backoff_seconds)
341
+ retry_count += 1
342
+
343
+ # All retries exhausted
344
+ duration_ms = (time.perf_counter() - start_time) * 1000
345
+ logger.error(
346
+ "Slack alert delivery failed after retries",
347
+ extra={
348
+ "correlation_id": str(correlation_id),
349
+ "duration_ms": round(duration_ms, 2),
350
+ "retry_count": retry_count,
351
+ "error_code": last_error_code,
352
+ },
353
+ )
354
+
355
+ return ModelSlackAlertResult(
356
+ success=False,
357
+ duration_ms=duration_ms,
358
+ correlation_id=correlation_id,
359
+ error=last_error,
360
+ error_code=last_error_code,
361
+ retry_count=retry_count,
362
+ )
363
+
364
+ def _format_block_kit_message(self, alert: ModelSlackAlert) -> dict[str, object]:
365
+ """Format alert as Slack Block Kit message.
366
+
367
+ Creates a rich formatted message using Slack's Block Kit API
368
+ with header, message body, and optional detail fields.
369
+
370
+ Args:
371
+ alert: Alert payload to format
372
+
373
+ Returns:
374
+ Dict containing Slack Block Kit blocks structure
375
+ """
376
+ emoji = _SEVERITY_EMOJI.get(alert.severity, ":white_circle:")
377
+ title = alert.title or _SEVERITY_TITLES.get(alert.severity, "Alert")
378
+
379
+ blocks: list[dict[str, object]] = [
380
+ {
381
+ "type": "header",
382
+ "text": {
383
+ "type": "plain_text",
384
+ "text": f"{emoji} {title}",
385
+ "emoji": True,
386
+ },
387
+ },
388
+ {"type": "divider"},
389
+ {
390
+ "type": "section",
391
+ "text": {
392
+ "type": "mrkdwn",
393
+ "text": alert.message[:3000], # Slack limit
394
+ },
395
+ },
396
+ ]
397
+
398
+ # Add detail fields if provided
399
+ if alert.details:
400
+ fields: list[dict[str, object]] = []
401
+ for key, value in list(alert.details.items())[:10]: # Limit to 10 fields
402
+ # Convert value to string, truncate if needed
403
+ value_str = str(value)[:100]
404
+ fields.append({"type": "mrkdwn", "text": f"*{key}:*\n{value_str}"})
405
+
406
+ # Slack allows max 10 fields per section
407
+ if fields:
408
+ blocks.append({"type": "section", "fields": fields})
409
+
410
+ # Add correlation ID for traceability
411
+ blocks.append(
412
+ {
413
+ "type": "context",
414
+ "elements": [
415
+ {
416
+ "type": "mrkdwn",
417
+ "text": f"Correlation: `{str(alert.correlation_id)[:16]}...`",
418
+ }
419
+ ],
420
+ }
421
+ )
422
+
423
+ return {"blocks": blocks}
424
+
425
+
426
+ __all__: list[str] = ["HandlerSlackWebhook"]
@@ -90,6 +90,11 @@ Manifest Persistence Models:
90
90
  ModelManifestQueryPayload: Payload for manifest.query operation
91
91
  ModelManifestQueryResult: Result from manifest.query operation
92
92
  ModelManifestMetadata: Lightweight metadata for manifest queries
93
+
94
+ Slack Models:
95
+ EnumAlertSeverity: Alert severity levels (critical, error, warning, info)
96
+ ModelSlackAlert: Input payload for Slack alert operations
97
+ ModelSlackAlertResult: Response from Slack webhook operations
93
98
  """
94
99
 
95
100
  from omnibase_infra.handlers.models.consul import (
@@ -104,6 +109,7 @@ from omnibase_infra.handlers.models.consul import (
104
109
  ModelConsulKVPutPayload,
105
110
  ModelConsulRegisterPayload,
106
111
  )
112
+ from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
107
113
  from omnibase_infra.handlers.models.http import (
108
114
  EnumHttpOperationType,
109
115
  HttpPayload,
@@ -198,6 +204,10 @@ from omnibase_infra.handlers.models.model_qdrant_handler_response import (
198
204
  ModelQdrantHandlerResponse,
199
205
  )
200
206
  from omnibase_infra.handlers.models.model_retry_state import ModelRetryState
207
+ from omnibase_infra.handlers.models.model_slack_alert_payload import ModelSlackAlert
208
+ from omnibase_infra.handlers.models.model_slack_alert_result import (
209
+ ModelSlackAlertResult,
210
+ )
201
211
  from omnibase_infra.handlers.models.model_vault_handler_response import (
202
212
  ModelVaultHandlerResponse,
203
213
  )
@@ -283,4 +293,8 @@ __all__: list[str] = [
283
293
  "ModelManifestQueryPayload",
284
294
  "ModelManifestQueryResult",
285
295
  "ModelManifestMetadata",
296
+ # Slack models
297
+ "EnumAlertSeverity",
298
+ "ModelSlackAlert",
299
+ "ModelSlackAlertResult",
286
300
  ]
@@ -0,0 +1,36 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Alert Severity Enumeration.
4
+
5
+ Defines severity levels for Slack alerts with corresponding
6
+ visual indicators for Block Kit formatting.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+
13
+
14
+ class EnumAlertSeverity(str, Enum):
15
+ """Alert severity levels for Slack notifications.
16
+
17
+ Maps to visual indicators in Slack Block Kit messages:
18
+ - CRITICAL: Red circle emoji (immediate attention required)
19
+ - ERROR: Red circle emoji (error occurred)
20
+ - WARNING: Yellow circle emoji (potential issue)
21
+ - INFO: Blue circle emoji (informational)
22
+
23
+ Attributes:
24
+ CRITICAL: System-critical issue requiring immediate attention
25
+ ERROR: Error condition that needs investigation
26
+ WARNING: Warning condition that may need attention
27
+ INFO: Informational message
28
+ """
29
+
30
+ CRITICAL = "critical"
31
+ ERROR = "error"
32
+ WARNING = "warning"
33
+ INFO = "info"
34
+
35
+
36
+ __all__ = ["EnumAlertSeverity"]
@@ -0,0 +1,24 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Alert Models Re-export.
4
+
5
+ This module re-exports Slack alert models from their individual files
6
+ to maintain backwards compatibility with existing imports.
7
+
8
+ Models are split per ONEX convention (one model per file):
9
+ - enum_alert_severity.py: EnumAlertSeverity
10
+ - model_slack_alert_payload.py: ModelSlackAlert
11
+ - model_slack_alert_result.py: ModelSlackAlertResult
12
+ """
13
+
14
+ from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
15
+ from omnibase_infra.handlers.models.model_slack_alert_payload import ModelSlackAlert
16
+ from omnibase_infra.handlers.models.model_slack_alert_result import (
17
+ ModelSlackAlertResult,
18
+ )
19
+
20
+ __all__ = [
21
+ "EnumAlertSeverity",
22
+ "ModelSlackAlert",
23
+ "ModelSlackAlertResult",
24
+ ]