omnibase_infra 0.2.1__py3-none-any.whl → 0.2.2__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 (116) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
  3. omnibase_infra/cli/commands.py +1 -1
  4. omnibase_infra/configs/widget_mapping.yaml +176 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
  7. omnibase_infra/errors/error_compute_registry.py +4 -1
  8. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  9. omnibase_infra/errors/error_infra.py +3 -1
  10. omnibase_infra/errors/error_policy_registry.py +4 -1
  11. omnibase_infra/handlers/handler_db.py +2 -1
  12. omnibase_infra/handlers/handler_graph.py +10 -5
  13. omnibase_infra/handlers/handler_mcp.py +736 -63
  14. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  15. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  16. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
  17. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  18. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  19. omnibase_infra/mixins/mixin_node_introspection.py +24 -7
  20. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  21. omnibase_infra/models/handlers/__init__.py +10 -0
  22. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  23. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  24. omnibase_infra/models/mcp/__init__.py +15 -0
  25. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  26. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  27. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  28. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  29. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  30. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  31. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  32. omnibase_infra/nodes/effects/contract.yaml +0 -5
  33. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  36. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  37. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  38. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  39. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
  40. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  41. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  42. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
  43. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  44. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  45. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  46. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  47. omnibase_infra/runtime/__init__.py +51 -1
  48. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  49. omnibase_infra/runtime/constants_notification.py +75 -0
  50. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  51. omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
  52. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  53. omnibase_infra/runtime/handler_contract_source.py +289 -167
  54. omnibase_infra/runtime/handler_plugin_loader.py +4 -2
  55. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  56. omnibase_infra/runtime/mixins/__init__.py +7 -0
  57. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  58. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  59. omnibase_infra/runtime/models/__init__.py +24 -0
  60. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  61. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  62. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  63. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  64. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  65. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  66. omnibase_infra/runtime/projector_shell.py +229 -1
  67. omnibase_infra/runtime/protocols/__init__.py +10 -0
  68. omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
  69. omnibase_infra/runtime/registry_policy.py +9 -326
  70. omnibase_infra/runtime/secret_resolver.py +4 -2
  71. omnibase_infra/runtime/service_kernel.py +10 -2
  72. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  73. omnibase_infra/runtime/service_runtime_host_process.py +225 -15
  74. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  75. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  76. omnibase_infra/runtime/util_container_wiring.py +6 -5
  77. omnibase_infra/runtime/util_wiring.py +5 -1
  78. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  79. omnibase_infra/services/mcp/__init__.py +31 -0
  80. omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
  81. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  82. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  83. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  84. omnibase_infra/services/registry_api/__init__.py +40 -0
  85. omnibase_infra/services/registry_api/main.py +243 -0
  86. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  87. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  88. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  89. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  90. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  91. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  92. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  93. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  94. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  95. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  96. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  97. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  98. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  99. omnibase_infra/services/registry_api/routes.py +371 -0
  100. omnibase_infra/services/registry_api/service.py +846 -0
  101. omnibase_infra/services/service_capability_query.py +4 -4
  102. omnibase_infra/services/service_health.py +3 -2
  103. omnibase_infra/services/service_timeout_emitter.py +13 -2
  104. omnibase_infra/utils/util_dsn_validation.py +1 -1
  105. omnibase_infra/validation/__init__.py +3 -19
  106. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  107. omnibase_infra/validation/infra_validators.py +35 -24
  108. omnibase_infra/validation/validation_exemptions.yaml +113 -9
  109. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  110. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  111. omnibase_infra/validation/validator_security.py +473 -370
  112. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
  113. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
  114. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
  115. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
  116. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,60 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Registry summary model for aggregate statistics.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+
14
+ class ModelRegistrySummary(BaseModel):
15
+ """Summary statistics for the registry.
16
+
17
+ Provides aggregate counts for dashboard widgets.
18
+
19
+ Attributes:
20
+ total_nodes: Total number of registered nodes
21
+ active_nodes: Number of nodes in ACTIVE state
22
+ healthy_instances: Number of passing health check instances
23
+ unhealthy_instances: Number of failing health check instances
24
+ by_node_type: Count of nodes by type
25
+ by_state: Count of nodes by registration state
26
+ """
27
+
28
+ model_config = ConfigDict(frozen=True, extra="forbid")
29
+
30
+ total_nodes: int = Field(
31
+ ...,
32
+ ge=0,
33
+ description="Total number of registered nodes",
34
+ )
35
+ active_nodes: int = Field(
36
+ ...,
37
+ ge=0,
38
+ description="Number of nodes in ACTIVE state",
39
+ )
40
+ healthy_instances: int = Field(
41
+ ...,
42
+ ge=0,
43
+ description="Number of passing health check instances",
44
+ )
45
+ unhealthy_instances: int = Field(
46
+ ...,
47
+ ge=0,
48
+ description="Number of failing health check instances",
49
+ )
50
+ by_node_type: dict[str, int] = Field(
51
+ default_factory=dict,
52
+ description="Count of nodes by type",
53
+ )
54
+ by_state: dict[str, int] = Field(
55
+ default_factory=dict,
56
+ description="Count of nodes by registration state",
57
+ )
58
+
59
+
60
+ __all__ = ["ModelRegistrySummary"]
@@ -0,0 +1,43 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Response model for list_instances endpoint.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ - PR #182: Add Pydantic response models for API endpoints
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from omnibase_infra.services.registry_api.models.model_registry_instance_view import (
15
+ ModelRegistryInstanceView,
16
+ )
17
+ from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
18
+
19
+
20
+ class ModelResponseListInstances(BaseModel):
21
+ """Response model for the GET /registry/instances endpoint.
22
+
23
+ Provides a list of live Consul service instances with optional warnings
24
+ for partial success scenarios.
25
+
26
+ Attributes:
27
+ instances: List of live Consul service instances
28
+ warnings: List of warnings for partial success scenarios
29
+ """
30
+
31
+ model_config = ConfigDict(frozen=True, extra="forbid")
32
+
33
+ instances: list[ModelRegistryInstanceView] = Field(
34
+ default_factory=list,
35
+ description="List of live Consul service instances",
36
+ )
37
+ warnings: list[ModelWarning] = Field(
38
+ default_factory=list,
39
+ description="Warnings for partial success scenarios",
40
+ )
41
+
42
+
43
+ __all__ = ["ModelResponseListInstances"]
@@ -0,0 +1,51 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Response model for list_nodes endpoint.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ - PR #182: Add Pydantic response models for API endpoints
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from omnibase_infra.services.registry_api.models.model_pagination_info import (
15
+ ModelPaginationInfo,
16
+ )
17
+ from omnibase_infra.services.registry_api.models.model_registry_node_view import (
18
+ ModelRegistryNodeView,
19
+ )
20
+ from omnibase_infra.services.registry_api.models.model_warning import ModelWarning
21
+
22
+
23
+ class ModelResponseListNodes(BaseModel):
24
+ """Response model for the GET /registry/nodes endpoint.
25
+
26
+ Provides a paginated list of registered nodes with optional warnings
27
+ for partial success scenarios.
28
+
29
+ Attributes:
30
+ nodes: List of registered nodes matching the query
31
+ pagination: Pagination information for the result set
32
+ warnings: List of warnings for partial success scenarios
33
+ """
34
+
35
+ model_config = ConfigDict(frozen=True, extra="forbid")
36
+
37
+ nodes: list[ModelRegistryNodeView] = Field(
38
+ default_factory=list,
39
+ description="List of registered nodes matching the query",
40
+ )
41
+ pagination: ModelPaginationInfo = Field(
42
+ ...,
43
+ description="Pagination information for the result set",
44
+ )
45
+ warnings: list[ModelWarning] = Field(
46
+ default_factory=list,
47
+ description="Warnings for partial success scenarios",
48
+ )
49
+
50
+
51
+ __all__ = ["ModelResponseListNodes"]
@@ -0,0 +1,49 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Warning model for partial success scenarios.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+
16
+ class ModelWarning(BaseModel):
17
+ """Warning message for partial success scenarios.
18
+
19
+ Used when one backend succeeds but another fails, allowing the
20
+ API to return partial results with appropriate warnings.
21
+
22
+ Attributes:
23
+ source: Source of the warning (e.g., "consul", "postgres")
24
+ message: Human-readable warning message
25
+ code: Optional error code for programmatic handling
26
+ timestamp: When the warning was generated
27
+ """
28
+
29
+ model_config = ConfigDict(frozen=True, extra="forbid")
30
+
31
+ source: str = Field(
32
+ ...,
33
+ description="Source of the warning",
34
+ )
35
+ message: str = Field(
36
+ ...,
37
+ description="Human-readable warning message",
38
+ )
39
+ code: str | None = Field(
40
+ default=None,
41
+ description="Optional error code",
42
+ )
43
+ timestamp: datetime = Field(
44
+ ...,
45
+ description="When the warning was generated",
46
+ )
47
+
48
+
49
+ __all__ = ["ModelWarning"]
@@ -0,0 +1,28 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Widget defaults model for dashboard configuration.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pydantic import BaseModel, ConfigDict
12
+
13
+
14
+ class ModelWidgetDefaults(BaseModel):
15
+ """Default configuration for a widget type.
16
+
17
+ Attributes vary by widget type and provide sensible defaults.
18
+ """
19
+
20
+ model_config = ConfigDict(frozen=True, extra="allow")
21
+
22
+ # Common fields - all optional
23
+ show_timestamp: bool | None = None
24
+ max_items: int | None = None
25
+ refresh_interval_seconds: int | None = None
26
+
27
+
28
+ __all__ = ["ModelWidgetDefaults"]
@@ -0,0 +1,51 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Widget mapping model for complete dashboard configuration.
4
+
5
+ Related Tickets:
6
+ - OMN-1278: Contract-Driven Dashboard - Registry Discovery
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ from omnibase_infra.services.registry_api.models.model_capability_widget_mapping import (
14
+ ModelCapabilityWidgetMapping,
15
+ )
16
+
17
+
18
+ class ModelWidgetMapping(BaseModel):
19
+ """Complete widget mapping configuration.
20
+
21
+ Loaded from widget_mapping.yaml, provides the mapping from
22
+ capabilities and semantic roles to widget types.
23
+
24
+ Attributes:
25
+ version: Configuration version for compatibility checking
26
+ capability_mappings: Map of capability tags to widget configs
27
+ semantic_mappings: Map of semantic roles to widget configs
28
+ fallback: Default widget config when no mapping matches
29
+ """
30
+
31
+ model_config = ConfigDict(frozen=True, extra="forbid")
32
+
33
+ version: str = Field(
34
+ ...,
35
+ description="Configuration version",
36
+ )
37
+ capability_mappings: dict[str, ModelCapabilityWidgetMapping] = Field(
38
+ default_factory=dict,
39
+ description="Map of capability tags to widget configs",
40
+ )
41
+ semantic_mappings: dict[str, ModelCapabilityWidgetMapping] = Field(
42
+ default_factory=dict,
43
+ description="Map of semantic roles to widget configs",
44
+ )
45
+ fallback: ModelCapabilityWidgetMapping = Field(
46
+ ...,
47
+ description="Default widget config when no mapping matches",
48
+ )
49
+
50
+
51
+ __all__ = ["ModelWidgetMapping"]
@@ -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"]