omnibase_infra 0.3.1__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 (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ ]
@@ -0,0 +1,77 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Alert Input Payload Model.
4
+
5
+ Provides the input payload model for Slack alert operations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from uuid import UUID, uuid4
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from omnibase_core.types import JsonType
15
+ from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
16
+
17
+
18
+ class ModelSlackAlert(BaseModel):
19
+ """Input payload for Slack alert operations.
20
+
21
+ This model defines the structure of alert payloads sent to
22
+ the HandlerSlackWebhook. The handler transforms this into
23
+ Slack Block Kit formatted messages.
24
+
25
+ Attributes:
26
+ severity: Alert severity level for visual formatting
27
+ message: Main alert message (required)
28
+ title: Optional alert title (defaults to severity-based title)
29
+ details: Optional key-value details to include in the alert
30
+ channel: Optional channel override (webhook default if not specified)
31
+ correlation_id: UUID for distributed tracing
32
+
33
+ Example:
34
+ >>> alert = ModelSlackAlert(
35
+ ... severity=EnumAlertSeverity.WARNING,
36
+ ... message="High memory usage detected",
37
+ ... title="Resource Alert",
38
+ ... details={"node": "registry-effect", "memory_pct": "85"},
39
+ ... )
40
+ """
41
+
42
+ model_config = ConfigDict(
43
+ frozen=True,
44
+ extra="forbid",
45
+ from_attributes=True, # Support pytest-xdist compatibility
46
+ )
47
+
48
+ severity: EnumAlertSeverity = Field(
49
+ default=EnumAlertSeverity.INFO,
50
+ description="Alert severity level for visual formatting",
51
+ )
52
+ message: str = Field(
53
+ ...,
54
+ min_length=1,
55
+ max_length=3000,
56
+ description="Main alert message content",
57
+ )
58
+ title: str | None = Field(
59
+ default=None,
60
+ max_length=150,
61
+ description="Optional alert title (defaults to severity-based title)",
62
+ )
63
+ details: dict[str, JsonType] = Field(
64
+ default_factory=dict,
65
+ description="Additional key-value details to include in the alert",
66
+ )
67
+ channel: str | None = Field(
68
+ default=None,
69
+ description="Optional channel override (uses webhook default if not specified)",
70
+ )
71
+ correlation_id: UUID = Field(
72
+ default_factory=uuid4,
73
+ description="UUID for distributed tracing",
74
+ )
75
+
76
+
77
+ __all__ = ["ModelSlackAlert"]
@@ -0,0 +1,73 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Alert Result Model.
4
+
5
+ Provides the response model for Slack webhook operations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from uuid import UUID
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+
15
+ class ModelSlackAlertResult(BaseModel):
16
+ """Response from Slack webhook operations.
17
+
18
+ This model captures the result of a Slack alert send operation,
19
+ including success/failure status, timing, and retry information.
20
+
21
+ Attributes:
22
+ success: Whether the alert was delivered successfully
23
+ duration_ms: Time taken for the operation in milliseconds
24
+ correlation_id: UUID from the original request for tracing
25
+ error: Sanitized error message if success is False
26
+ error_code: Error code for programmatic handling
27
+ retry_count: Number of retry attempts made
28
+
29
+ Example:
30
+ >>> result = ModelSlackAlertResult(
31
+ ... success=True,
32
+ ... duration_ms=123.45,
33
+ ... correlation_id=uuid4(),
34
+ ... )
35
+ >>> print(result.success)
36
+ True
37
+ """
38
+
39
+ model_config = ConfigDict(
40
+ frozen=True,
41
+ extra="forbid",
42
+ from_attributes=True,
43
+ )
44
+
45
+ success: bool = Field(
46
+ ...,
47
+ description="Whether the alert was delivered successfully",
48
+ )
49
+ duration_ms: float = Field(
50
+ default=0.0,
51
+ ge=0.0,
52
+ description="Time taken for the operation in milliseconds",
53
+ )
54
+ correlation_id: UUID = Field(
55
+ ...,
56
+ description="UUID from the original request for tracing",
57
+ )
58
+ error: str | None = Field(
59
+ default=None,
60
+ description="Sanitized error message if success is False",
61
+ )
62
+ error_code: str | None = Field(
63
+ default=None,
64
+ description="Error code for programmatic handling",
65
+ )
66
+ retry_count: int = Field(
67
+ default=0,
68
+ ge=0,
69
+ description="Number of retry attempts made",
70
+ )
71
+
72
+
73
+ __all__ = ["ModelSlackAlertResult"]