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,371 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Registry API Routes.
4
+
5
+ FastAPI route handlers for the Registry API. Routes are defined as an
6
+ APIRouter for easy mounting into the main FastAPI application.
7
+
8
+ Endpoint Summary:
9
+ GET /registry/discovery - Full dashboard payload
10
+ GET /registry/nodes - Node list with pagination
11
+ GET /registry/nodes/{id} - Single node detail
12
+ GET /registry/instances - Live Consul instances
13
+ GET /registry/widgets/mapping - Widget mapping configuration
14
+ GET /registry/health - Service health check
15
+
16
+ Related Tickets:
17
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import TYPE_CHECKING, Annotated
23
+ from uuid import UUID, uuid4
24
+
25
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
26
+
27
+ from omnibase_infra.enums import EnumRegistrationState
28
+ from omnibase_infra.services.registry_api.models import (
29
+ ModelRegistryDiscoveryResponse,
30
+ ModelRegistryHealthResponse,
31
+ ModelRegistryNodeView,
32
+ ModelResponseListInstances,
33
+ ModelResponseListNodes,
34
+ ModelWidgetMapping,
35
+ )
36
+
37
+ if TYPE_CHECKING:
38
+ from omnibase_infra.services.registry_api.service import ServiceRegistryDiscovery
39
+
40
+
41
+ def get_correlation_id(
42
+ x_correlation_id: Annotated[
43
+ str | None,
44
+ Header(
45
+ alias="X-Correlation-ID",
46
+ description="Correlation ID for distributed tracing. Must be a valid UUID if provided.",
47
+ ),
48
+ ] = None,
49
+ ) -> UUID:
50
+ """FastAPI dependency to extract and validate correlation ID from HTTP header.
51
+
52
+ Extracts the X-Correlation-ID header value and validates it as a UUID.
53
+ If no header is provided, generates a new UUID for the request.
54
+
55
+ Args:
56
+ x_correlation_id: Optional correlation ID string from X-Correlation-ID header.
57
+
58
+ Returns:
59
+ Parsed UUID from header or newly generated UUID if not provided.
60
+
61
+ Raises:
62
+ HTTPException: 400 Bad Request if correlation ID is provided but not a valid UUID.
63
+
64
+ Example:
65
+ Valid header: X-Correlation-ID: 550e8400-e29b-41d4-a716-446655440000
66
+ Invalid header: X-Correlation-ID: not-a-uuid (returns 400)
67
+ Missing header: Generates new UUID automatically
68
+ """
69
+ if x_correlation_id is None:
70
+ return uuid4()
71
+ try:
72
+ return UUID(x_correlation_id)
73
+ except ValueError:
74
+ raise HTTPException(
75
+ status_code=status.HTTP_400_BAD_REQUEST,
76
+ detail=f"Invalid X-Correlation-ID header format: '{x_correlation_id}'. Must be a valid UUID (e.g., '550e8400-e29b-41d4-a716-446655440000').",
77
+ ) from None
78
+
79
+
80
+ # Create router with prefix
81
+ router = APIRouter(
82
+ prefix="/registry",
83
+ tags=["registry"],
84
+ responses={
85
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
86
+ 500: {"description": "Internal server error"},
87
+ 503: {"description": "Service unavailable"},
88
+ },
89
+ )
90
+
91
+
92
+ def get_service(request: Request) -> ServiceRegistryDiscovery:
93
+ """Dependency to get the registry discovery service from app state.
94
+
95
+ Args:
96
+ request: FastAPI request object.
97
+
98
+ Returns:
99
+ ServiceRegistryDiscovery instance from app state.
100
+
101
+ Raises:
102
+ HTTPException: If service is not configured in app state.
103
+ """
104
+ service: ServiceRegistryDiscovery | None = getattr(
105
+ request.app.state, "registry_service", None
106
+ )
107
+ if service is None:
108
+ raise HTTPException(
109
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
110
+ detail="Registry service not configured",
111
+ )
112
+ return service
113
+
114
+
115
+ @router.get(
116
+ "/discovery",
117
+ response_model=ModelRegistryDiscoveryResponse,
118
+ summary="Full Dashboard Payload",
119
+ description=(
120
+ "Returns the complete dashboard payload including nodes, live instances, "
121
+ "and summary statistics. This is the primary endpoint for dashboard "
122
+ "consumption, providing all needed data in a single request."
123
+ ),
124
+ responses={
125
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
126
+ 200: {
127
+ "description": "Successful response with full discovery data",
128
+ "content": {
129
+ "application/json": {
130
+ "example": {
131
+ "timestamp": "2025-01-21T10:00:00Z",
132
+ "warnings": [],
133
+ "summary": {
134
+ "total_nodes": 10,
135
+ "active_nodes": 8,
136
+ "healthy_instances": 5,
137
+ "unhealthy_instances": 2,
138
+ "by_node_type": {"EFFECT": 5, "COMPUTE": 3, "REDUCER": 2},
139
+ "by_state": {"active": 8, "pending_registration": 2},
140
+ },
141
+ "nodes": [],
142
+ "live_instances": [],
143
+ "pagination": {
144
+ "total": 10,
145
+ "limit": 100,
146
+ "offset": 0,
147
+ "has_more": False,
148
+ },
149
+ }
150
+ }
151
+ },
152
+ },
153
+ },
154
+ )
155
+ async def get_discovery(
156
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
157
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
158
+ limit: Annotated[
159
+ int,
160
+ Query(ge=1, le=1000, description="Maximum number of nodes to return"),
161
+ ] = 100,
162
+ offset: Annotated[
163
+ int,
164
+ Query(ge=0, description="Number of nodes to skip for pagination"),
165
+ ] = 0,
166
+ ) -> ModelRegistryDiscoveryResponse:
167
+ """Get full dashboard payload with nodes, instances, and summary."""
168
+ response = await service.get_discovery(
169
+ limit=limit,
170
+ offset=offset,
171
+ correlation_id=correlation_id,
172
+ )
173
+
174
+ return response
175
+
176
+
177
+ @router.get(
178
+ "/nodes",
179
+ response_model=ModelResponseListNodes,
180
+ summary="List Registered Nodes",
181
+ description=(
182
+ "Returns a paginated list of registered nodes from the PostgreSQL "
183
+ "projection store. Supports filtering by state and node type."
184
+ ),
185
+ responses={
186
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
187
+ 200: {
188
+ "description": "Successful response with node list",
189
+ },
190
+ },
191
+ )
192
+ async def list_nodes(
193
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
194
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
195
+ limit: Annotated[
196
+ int,
197
+ Query(ge=1, le=1000, description="Maximum number of nodes to return"),
198
+ ] = 100,
199
+ offset: Annotated[
200
+ int,
201
+ Query(ge=0, description="Number of nodes to skip for pagination"),
202
+ ] = 0,
203
+ state: Annotated[
204
+ str | None,
205
+ Query(
206
+ description="Filter by registration state (e.g., 'active', 'pending_registration')"
207
+ ),
208
+ ] = None,
209
+ node_type: Annotated[
210
+ str | None,
211
+ Query(
212
+ description="Filter by node type (effect, compute, reducer, orchestrator)"
213
+ ),
214
+ ] = None,
215
+ ) -> ModelResponseListNodes:
216
+ """List registered nodes with pagination and optional filtering."""
217
+ # Parse state filter
218
+ state_filter: EnumRegistrationState | None = None
219
+ if state is not None:
220
+ try:
221
+ state_filter = EnumRegistrationState(state)
222
+ except ValueError:
223
+ raise HTTPException(
224
+ status_code=status.HTTP_400_BAD_REQUEST,
225
+ detail=f"Invalid state value: {state}. Valid values: {[s.value for s in EnumRegistrationState]}",
226
+ ) from None
227
+
228
+ nodes, pagination, warnings = await service.list_nodes(
229
+ limit=limit,
230
+ offset=offset,
231
+ state=state_filter,
232
+ node_type=node_type,
233
+ correlation_id=correlation_id,
234
+ )
235
+
236
+ return ModelResponseListNodes(
237
+ nodes=nodes,
238
+ pagination=pagination,
239
+ warnings=warnings,
240
+ )
241
+
242
+
243
+ @router.get(
244
+ "/nodes/{node_id}",
245
+ response_model=ModelRegistryNodeView,
246
+ summary="Get Node Details",
247
+ description="Returns detailed information for a single registered node by ID.",
248
+ responses={
249
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
250
+ 200: {"description": "Successful response with node details"},
251
+ 404: {"description": "Node not found"},
252
+ },
253
+ )
254
+ async def get_node(
255
+ node_id: UUID,
256
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
257
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
258
+ ) -> ModelRegistryNodeView:
259
+ """Get a single node by ID."""
260
+
261
+ node, warnings = await service.get_node(
262
+ node_id=node_id,
263
+ correlation_id=correlation_id,
264
+ )
265
+
266
+ if node is None:
267
+ # Check if it was a service error or genuinely not found
268
+ if warnings:
269
+ raise HTTPException(
270
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
271
+ detail=f"Service error: {warnings[0].message}",
272
+ )
273
+ raise HTTPException(
274
+ status_code=status.HTTP_404_NOT_FOUND,
275
+ detail=f"Node not found: {node_id}",
276
+ )
277
+
278
+ return node
279
+
280
+
281
+ @router.get(
282
+ "/instances",
283
+ response_model=ModelResponseListInstances,
284
+ summary="List Live Consul Instances",
285
+ description=(
286
+ "Returns a list of live service instances from Consul. "
287
+ "Includes health status and metadata for each instance."
288
+ ),
289
+ responses={
290
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
291
+ 200: {"description": "Successful response with instance list"},
292
+ },
293
+ )
294
+ async def list_instances(
295
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
296
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
297
+ service_name: Annotated[
298
+ str | None,
299
+ Query(description="Filter by service name"),
300
+ ] = None,
301
+ include_unhealthy: Annotated[
302
+ bool,
303
+ Query(description="Include unhealthy instances in results"),
304
+ ] = False,
305
+ ) -> ModelResponseListInstances:
306
+ """List live Consul service instances."""
307
+ instances, warnings = await service.list_instances(
308
+ service_name=service_name,
309
+ include_unhealthy=include_unhealthy,
310
+ correlation_id=correlation_id,
311
+ )
312
+
313
+ return ModelResponseListInstances(
314
+ instances=instances,
315
+ warnings=warnings,
316
+ )
317
+
318
+
319
+ @router.get(
320
+ "/widgets/mapping",
321
+ response_model=ModelWidgetMapping,
322
+ summary="Widget Mapping Configuration",
323
+ description=(
324
+ "Returns the capability-to-widget mapping configuration. "
325
+ "Used by dashboards to determine which widget type to render "
326
+ "for each node capability."
327
+ ),
328
+ responses={
329
+ 200: {"description": "Successful response with widget mapping"},
330
+ 503: {"description": "Configuration not available"},
331
+ },
332
+ )
333
+ async def get_widget_mapping(
334
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
335
+ ) -> ModelWidgetMapping:
336
+ """Get widget mapping configuration."""
337
+ mapping, warnings = service.get_widget_mapping()
338
+
339
+ if mapping is None:
340
+ raise HTTPException(
341
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
342
+ detail=f"Widget mapping not available: {warnings[0].message if warnings else 'Unknown error'}",
343
+ )
344
+
345
+ return mapping
346
+
347
+
348
+ @router.get(
349
+ "/health",
350
+ response_model=ModelRegistryHealthResponse,
351
+ summary="Service Health Check",
352
+ description=(
353
+ "Performs a health check on all backend components (PostgreSQL, Consul, config) "
354
+ "and returns the overall service health status."
355
+ ),
356
+ responses={
357
+ 400: {"description": "Bad request (e.g., invalid correlation ID format)"},
358
+ 200: {"description": "Health check response (may indicate degraded/unhealthy)"},
359
+ },
360
+ )
361
+ async def health_check(
362
+ service: Annotated[ServiceRegistryDiscovery, Depends(get_service)],
363
+ correlation_id: Annotated[UUID, Depends(get_correlation_id)],
364
+ ) -> ModelRegistryHealthResponse:
365
+ """Perform health check on all backend components."""
366
+ response = await service.health_check(correlation_id=correlation_id)
367
+
368
+ return response
369
+
370
+
371
+ __all__ = ["router", "get_service", "get_correlation_id"]