mcp-hangar 0.2.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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
mcp_hangar/context.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Request context management using contextvars.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for binding contextual information to log entries
|
|
4
|
+
within a request scope. All logs emitted during request processing will automatically
|
|
5
|
+
include the bound context (request_id, server_name, tool_name, etc.).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from mcp_hangar.context import bind_request_context, clear_request_context
|
|
9
|
+
|
|
10
|
+
async def handle_request(request):
|
|
11
|
+
bind_request_context(
|
|
12
|
+
request_id=request.id,
|
|
13
|
+
server_name="filesystem",
|
|
14
|
+
tool_name="read_file",
|
|
15
|
+
)
|
|
16
|
+
try:
|
|
17
|
+
# All logs in this scope will include the bound context
|
|
18
|
+
logger.info("processing_request")
|
|
19
|
+
result = await process(request)
|
|
20
|
+
logger.info("request_completed")
|
|
21
|
+
return result
|
|
22
|
+
finally:
|
|
23
|
+
clear_request_context()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from contextvars import ContextVar
|
|
29
|
+
from typing import Any
|
|
30
|
+
import uuid
|
|
31
|
+
|
|
32
|
+
import structlog
|
|
33
|
+
|
|
34
|
+
# Context variables for request-scoped data
|
|
35
|
+
request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
36
|
+
server_name_var: ContextVar[str | None] = ContextVar("server_name", default=None)
|
|
37
|
+
tool_name_var: ContextVar[str | None] = ContextVar("tool_name", default=None)
|
|
38
|
+
user_id_var: ContextVar[str | None] = ContextVar("user_id", default=None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_request_id() -> str:
|
|
42
|
+
"""Generate a short unique request ID."""
|
|
43
|
+
return uuid.uuid4().hex[:12]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_request_id() -> str | None:
|
|
47
|
+
"""Get the current request ID from context."""
|
|
48
|
+
return request_id_var.get()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def bind_request_context(
|
|
52
|
+
request_id: str | None = None,
|
|
53
|
+
server_name: str | None = None,
|
|
54
|
+
tool_name: str | None = None,
|
|
55
|
+
user_id: str | None = None,
|
|
56
|
+
**extra: Any,
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Bind contextual information to all logs in the current scope.
|
|
59
|
+
|
|
60
|
+
This function sets context variables and binds them to structlog's contextvars,
|
|
61
|
+
ensuring all subsequent log entries include this information.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request_id: Unique identifier for the request. Auto-generated if not provided.
|
|
65
|
+
server_name: Name of the target server/provider.
|
|
66
|
+
tool_name: Name of the tool being invoked.
|
|
67
|
+
user_id: Optional user identifier for attribution.
|
|
68
|
+
**extra: Additional key-value pairs to include in log context.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The request_id (either provided or generated).
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
request_id = bind_request_context(
|
|
75
|
+
server_name="filesystem",
|
|
76
|
+
tool_name="read_file",
|
|
77
|
+
path="/tmp/test.txt",
|
|
78
|
+
)
|
|
79
|
+
"""
|
|
80
|
+
# Generate request_id if not provided
|
|
81
|
+
if request_id is None:
|
|
82
|
+
request_id = generate_request_id()
|
|
83
|
+
|
|
84
|
+
# Set context variables
|
|
85
|
+
request_id_var.set(request_id)
|
|
86
|
+
if server_name is not None:
|
|
87
|
+
server_name_var.set(server_name)
|
|
88
|
+
if tool_name is not None:
|
|
89
|
+
tool_name_var.set(tool_name)
|
|
90
|
+
if user_id is not None:
|
|
91
|
+
user_id_var.set(user_id)
|
|
92
|
+
|
|
93
|
+
# Build context dict for structlog
|
|
94
|
+
context: dict[str, Any] = {"request_id": request_id}
|
|
95
|
+
if server_name is not None:
|
|
96
|
+
context["server"] = server_name
|
|
97
|
+
if tool_name is not None:
|
|
98
|
+
context["tool"] = tool_name
|
|
99
|
+
if user_id is not None:
|
|
100
|
+
context["user_id"] = user_id
|
|
101
|
+
context.update(extra)
|
|
102
|
+
|
|
103
|
+
# Clear any previous context and bind new one
|
|
104
|
+
structlog.contextvars.clear_contextvars()
|
|
105
|
+
structlog.contextvars.bind_contextvars(**context)
|
|
106
|
+
|
|
107
|
+
return request_id
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def update_request_context(**kwargs: Any) -> None:
|
|
111
|
+
"""Update the current request context with additional information.
|
|
112
|
+
|
|
113
|
+
This is useful for adding information that becomes available during processing,
|
|
114
|
+
such as the routed server name or response status.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
**kwargs: Key-value pairs to add to the context.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
update_request_context(routed_to="memory-server", status="success")
|
|
121
|
+
"""
|
|
122
|
+
structlog.contextvars.bind_contextvars(**kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def clear_request_context() -> None:
|
|
126
|
+
"""Clear all request-scoped context.
|
|
127
|
+
|
|
128
|
+
Should be called at the end of request processing to prevent context leakage.
|
|
129
|
+
"""
|
|
130
|
+
structlog.contextvars.clear_contextvars()
|
|
131
|
+
request_id_var.set(None)
|
|
132
|
+
server_name_var.set(None)
|
|
133
|
+
tool_name_var.set(None)
|
|
134
|
+
user_id_var.set(None)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RequestContextManager:
|
|
138
|
+
"""Context manager for automatic request context handling.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
async with RequestContextManager(tool_name="read_file") as ctx:
|
|
142
|
+
logger.info("processing")
|
|
143
|
+
# ctx.request_id is available
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
request_id: str | None = None,
|
|
149
|
+
server_name: str | None = None,
|
|
150
|
+
tool_name: str | None = None,
|
|
151
|
+
user_id: str | None = None,
|
|
152
|
+
**extra: Any,
|
|
153
|
+
):
|
|
154
|
+
self._request_id = request_id
|
|
155
|
+
self._server_name = server_name
|
|
156
|
+
self._tool_name = tool_name
|
|
157
|
+
self._user_id = user_id
|
|
158
|
+
self._extra = extra
|
|
159
|
+
self.request_id: str | None = None
|
|
160
|
+
|
|
161
|
+
def __enter__(self) -> "RequestContextManager":
|
|
162
|
+
self.request_id = bind_request_context(
|
|
163
|
+
request_id=self._request_id,
|
|
164
|
+
server_name=self._server_name,
|
|
165
|
+
tool_name=self._tool_name,
|
|
166
|
+
user_id=self._user_id,
|
|
167
|
+
**self._extra,
|
|
168
|
+
)
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
172
|
+
clear_request_context()
|
|
173
|
+
|
|
174
|
+
async def __aenter__(self) -> "RequestContextManager":
|
|
175
|
+
return self.__enter__()
|
|
176
|
+
|
|
177
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
178
|
+
self.__exit__(exc_type, exc_val, exc_tb)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Domain layer - Core business logic, events, and exceptions."""
|
|
2
|
+
|
|
3
|
+
from .events import (
|
|
4
|
+
DomainEvent,
|
|
5
|
+
HealthCheckFailed,
|
|
6
|
+
HealthCheckPassed,
|
|
7
|
+
ProviderDegraded,
|
|
8
|
+
ProviderIdleDetected,
|
|
9
|
+
ProviderStarted,
|
|
10
|
+
ProviderStateChanged,
|
|
11
|
+
ProviderStopped,
|
|
12
|
+
ToolInvocationCompleted,
|
|
13
|
+
ToolInvocationFailed,
|
|
14
|
+
ToolInvocationRequested,
|
|
15
|
+
)
|
|
16
|
+
from .exceptions import ( # Client; Base; Provider; Rate Limiting; Tool; Validation
|
|
17
|
+
CannotStartProviderError,
|
|
18
|
+
ClientError,
|
|
19
|
+
ClientNotConnectedError,
|
|
20
|
+
ClientTimeoutError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
InvalidStateTransitionError,
|
|
23
|
+
MCPError,
|
|
24
|
+
ProviderDegradedError,
|
|
25
|
+
ProviderError,
|
|
26
|
+
ProviderNotFoundError,
|
|
27
|
+
ProviderNotReadyError,
|
|
28
|
+
ProviderStartError,
|
|
29
|
+
RateLimitExceeded,
|
|
30
|
+
ToolError,
|
|
31
|
+
ToolInvocationError,
|
|
32
|
+
ToolNotFoundError,
|
|
33
|
+
ToolTimeoutError,
|
|
34
|
+
ValidationError,
|
|
35
|
+
)
|
|
36
|
+
from .repository import InMemoryProviderRepository, IProviderRepository
|
|
37
|
+
from .value_objects import ( # Configuration; Timing; Identity; Enums; Tool Arguments
|
|
38
|
+
CommandLine,
|
|
39
|
+
CorrelationId,
|
|
40
|
+
DockerImage,
|
|
41
|
+
Endpoint,
|
|
42
|
+
EnvironmentVariables,
|
|
43
|
+
HealthCheckInterval,
|
|
44
|
+
HealthStatus,
|
|
45
|
+
IdleTTL,
|
|
46
|
+
MaxConsecutiveFailures,
|
|
47
|
+
ProviderConfig,
|
|
48
|
+
ProviderId,
|
|
49
|
+
ProviderMode,
|
|
50
|
+
ProviderState,
|
|
51
|
+
TimeoutSeconds,
|
|
52
|
+
ToolArguments,
|
|
53
|
+
ToolName,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Events
|
|
58
|
+
"DomainEvent",
|
|
59
|
+
"ProviderStarted",
|
|
60
|
+
"ProviderStopped",
|
|
61
|
+
"ProviderDegraded",
|
|
62
|
+
"ProviderStateChanged",
|
|
63
|
+
"ToolInvocationRequested",
|
|
64
|
+
"ToolInvocationCompleted",
|
|
65
|
+
"ToolInvocationFailed",
|
|
66
|
+
"HealthCheckPassed",
|
|
67
|
+
"HealthCheckFailed",
|
|
68
|
+
"ProviderIdleDetected",
|
|
69
|
+
# Enums
|
|
70
|
+
"ProviderState",
|
|
71
|
+
"ProviderMode",
|
|
72
|
+
"HealthStatus",
|
|
73
|
+
# Value Objects - Identity
|
|
74
|
+
"ProviderId",
|
|
75
|
+
"ToolName",
|
|
76
|
+
"CorrelationId",
|
|
77
|
+
# Value Objects - Configuration
|
|
78
|
+
"CommandLine",
|
|
79
|
+
"DockerImage",
|
|
80
|
+
"Endpoint",
|
|
81
|
+
"EnvironmentVariables",
|
|
82
|
+
"ProviderConfig",
|
|
83
|
+
# Value Objects - Timing
|
|
84
|
+
"IdleTTL",
|
|
85
|
+
"HealthCheckInterval",
|
|
86
|
+
"MaxConsecutiveFailures",
|
|
87
|
+
"TimeoutSeconds",
|
|
88
|
+
# Value Objects - Tool Arguments
|
|
89
|
+
"ToolArguments",
|
|
90
|
+
# Exceptions - Base
|
|
91
|
+
"MCPError",
|
|
92
|
+
# Exceptions - Provider
|
|
93
|
+
"ProviderError",
|
|
94
|
+
"ProviderNotFoundError",
|
|
95
|
+
"ProviderStartError",
|
|
96
|
+
"ProviderDegradedError",
|
|
97
|
+
"CannotStartProviderError",
|
|
98
|
+
"ProviderNotReadyError",
|
|
99
|
+
"InvalidStateTransitionError",
|
|
100
|
+
# Exceptions - Tool
|
|
101
|
+
"ToolError",
|
|
102
|
+
"ToolNotFoundError",
|
|
103
|
+
"ToolInvocationError",
|
|
104
|
+
"ToolTimeoutError",
|
|
105
|
+
# Exceptions - Client
|
|
106
|
+
"ClientError",
|
|
107
|
+
"ClientNotConnectedError",
|
|
108
|
+
"ClientTimeoutError",
|
|
109
|
+
# Exceptions - Validation
|
|
110
|
+
"ValidationError",
|
|
111
|
+
"ConfigurationError",
|
|
112
|
+
# Exceptions - Rate Limiting
|
|
113
|
+
"RateLimitExceeded",
|
|
114
|
+
# Repository
|
|
115
|
+
"IProviderRepository",
|
|
116
|
+
"InMemoryProviderRepository",
|
|
117
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Domain contracts - interfaces for external dependencies.
|
|
2
|
+
|
|
3
|
+
This module defines contracts (abstract interfaces) that the domain layer
|
|
4
|
+
depends on. Implementations are provided by the infrastructure layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .authentication import ApiKeyMetadata, AuthRequest, IApiKeyStore, IAuthenticator, ITokenValidator
|
|
8
|
+
from .authorization import AuthorizationRequest, AuthorizationResult, IAuthorizer, IPolicyEngine, IRoleStore
|
|
9
|
+
from .event_store import ConcurrencyError, IEventStore, NullEventStore, StreamNotFoundError
|
|
10
|
+
from .metrics_publisher import IMetricsPublisher
|
|
11
|
+
from .persistence import (
|
|
12
|
+
AuditAction,
|
|
13
|
+
AuditEntry,
|
|
14
|
+
ConcurrentModificationError,
|
|
15
|
+
ConfigurationNotFoundError,
|
|
16
|
+
IAuditRepository,
|
|
17
|
+
IProviderConfigRepository,
|
|
18
|
+
IRecoveryService,
|
|
19
|
+
IUnitOfWork,
|
|
20
|
+
PersistenceError,
|
|
21
|
+
ProviderConfigSnapshot,
|
|
22
|
+
)
|
|
23
|
+
from .provider_runtime import ProviderRuntime
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Authentication contracts
|
|
27
|
+
"ApiKeyMetadata",
|
|
28
|
+
"AuthRequest",
|
|
29
|
+
"IApiKeyStore",
|
|
30
|
+
"IAuthenticator",
|
|
31
|
+
"ITokenValidator",
|
|
32
|
+
# Authorization contracts
|
|
33
|
+
"AuthorizationRequest",
|
|
34
|
+
"AuthorizationResult",
|
|
35
|
+
"IAuthorizer",
|
|
36
|
+
"IPolicyEngine",
|
|
37
|
+
"IRoleStore",
|
|
38
|
+
# Event store
|
|
39
|
+
"ConcurrencyError",
|
|
40
|
+
"IEventStore",
|
|
41
|
+
"NullEventStore",
|
|
42
|
+
"StreamNotFoundError",
|
|
43
|
+
# Metrics
|
|
44
|
+
"IMetricsPublisher",
|
|
45
|
+
# Persistence
|
|
46
|
+
"AuditAction",
|
|
47
|
+
"AuditEntry",
|
|
48
|
+
"ConcurrentModificationError",
|
|
49
|
+
"ConfigurationNotFoundError",
|
|
50
|
+
"IAuditRepository",
|
|
51
|
+
"IProviderConfigRepository",
|
|
52
|
+
"IRecoveryService",
|
|
53
|
+
"IUnitOfWork",
|
|
54
|
+
"PersistenceError",
|
|
55
|
+
"ProviderConfigSnapshot",
|
|
56
|
+
"ProviderRuntime",
|
|
57
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Authentication contracts (ports) for the domain layer.
|
|
2
|
+
|
|
3
|
+
These protocols define the interfaces for authentication components.
|
|
4
|
+
Infrastructure layer provides concrete implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
from ..value_objects import Principal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AuthRequest:
|
|
17
|
+
"""Normalized authentication request.
|
|
18
|
+
|
|
19
|
+
Contains all information needed to authenticate a request,
|
|
20
|
+
abstracted from the transport layer (HTTP, gRPC, etc.).
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
headers: Request headers as a case-insensitive dict.
|
|
24
|
+
source_ip: IP address of the request origin.
|
|
25
|
+
method: HTTP method or equivalent (GET, POST, etc.).
|
|
26
|
+
path: Request path/endpoint.
|
|
27
|
+
metadata: Additional context for authentication decisions.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
headers: dict[str, str]
|
|
31
|
+
source_ip: str
|
|
32
|
+
method: str = ""
|
|
33
|
+
path: str = ""
|
|
34
|
+
metadata: dict = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ApiKeyMetadata:
|
|
39
|
+
"""Metadata about an API key (never contains the actual key).
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
key_id: Unique identifier for the key (not the key itself).
|
|
43
|
+
name: Human-readable name for the key.
|
|
44
|
+
principal_id: Principal ID this key authenticates as.
|
|
45
|
+
created_at: When the key was created.
|
|
46
|
+
expires_at: Optional expiration datetime.
|
|
47
|
+
last_used_at: When the key was last used for authentication.
|
|
48
|
+
revoked: Whether the key has been revoked.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
key_id: str
|
|
52
|
+
name: str
|
|
53
|
+
principal_id: str
|
|
54
|
+
created_at: datetime
|
|
55
|
+
expires_at: datetime | None = None
|
|
56
|
+
last_used_at: datetime | None = None
|
|
57
|
+
revoked: bool = False
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_expired(self) -> bool:
|
|
61
|
+
"""Check if the key has expired."""
|
|
62
|
+
if self.expires_at is None:
|
|
63
|
+
return False
|
|
64
|
+
from datetime import timezone
|
|
65
|
+
|
|
66
|
+
return datetime.now(timezone.utc) > self.expires_at
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_valid(self) -> bool:
|
|
70
|
+
"""Check if the key is valid (not revoked and not expired)."""
|
|
71
|
+
return not self.revoked and not self.is_expired
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class IAuthenticator(Protocol):
|
|
76
|
+
"""Extracts and validates credentials from a request.
|
|
77
|
+
|
|
78
|
+
Authenticators are responsible for:
|
|
79
|
+
1. Detecting if they can handle a request (supports())
|
|
80
|
+
2. Extracting credentials from the request
|
|
81
|
+
3. Validating credentials
|
|
82
|
+
4. Returning an authenticated Principal
|
|
83
|
+
|
|
84
|
+
Multiple authenticators can be chained, with the first supporting
|
|
85
|
+
authenticator handling the request.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def authenticate(self, request: AuthRequest) -> Principal:
|
|
90
|
+
"""Authenticate a request and return the principal.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
request: The incoming request with credentials.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Authenticated Principal.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
InvalidCredentialsError: If credentials are invalid.
|
|
100
|
+
ExpiredCredentialsError: If credentials have expired.
|
|
101
|
+
RevokedCredentialsError: If credentials have been revoked.
|
|
102
|
+
MissingCredentialsError: If required credentials are missing.
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def supports(self, request: AuthRequest) -> bool:
|
|
108
|
+
"""Check if this authenticator can handle the request.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
request: The incoming request to check.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if this authenticator should handle the request.
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@runtime_checkable
|
|
120
|
+
class ITokenValidator(Protocol):
|
|
121
|
+
"""Validates tokens (JWT, API keys, etc.).
|
|
122
|
+
|
|
123
|
+
Token validators handle the cryptographic verification of tokens
|
|
124
|
+
without the transport-layer concerns of extracting them from requests.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def validate(self, token: str) -> dict:
|
|
129
|
+
"""Validate token and return claims.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
token: The token string to validate.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary of validated claims from the token.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
InvalidCredentialsError: If token is invalid or malformed.
|
|
139
|
+
ExpiredCredentialsError: If token has expired.
|
|
140
|
+
"""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@runtime_checkable
|
|
145
|
+
class IApiKeyStore(Protocol):
|
|
146
|
+
"""Storage for API keys.
|
|
147
|
+
|
|
148
|
+
Handles CRUD operations for API keys. Keys are stored as hashes,
|
|
149
|
+
never in plaintext. The raw key is only returned once during creation.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def get_principal_for_key(self, key_hash: str) -> Principal | None:
|
|
154
|
+
"""Look up principal for an API key hash.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
key_hash: SHA-256 hash of the API key.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Principal associated with the key, or None if not found.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ExpiredCredentialsError: If the key has expired.
|
|
164
|
+
RevokedCredentialsError: If the key has been revoked.
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def create_key(
|
|
170
|
+
self,
|
|
171
|
+
principal_id: str,
|
|
172
|
+
name: str,
|
|
173
|
+
expires_at: datetime | None = None,
|
|
174
|
+
groups: frozenset[str] | None = None,
|
|
175
|
+
tenant_id: str | None = None,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Create a new API key.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
principal_id: ID for the principal this key authenticates as.
|
|
181
|
+
name: Human-readable name for the key.
|
|
182
|
+
expires_at: Optional expiration datetime.
|
|
183
|
+
groups: Optional groups to assign to the principal.
|
|
184
|
+
tenant_id: Optional tenant ID for multi-tenancy.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The raw API key (only shown once!).
|
|
188
|
+
"""
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def revoke_key(self, key_id: str) -> bool:
|
|
193
|
+
"""Revoke an API key.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
key_id: Unique identifier of the key to revoke.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if key was found and revoked, False if not found.
|
|
200
|
+
"""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
def list_keys(self, principal_id: str) -> list[ApiKeyMetadata]:
|
|
205
|
+
"""List API keys for a principal (metadata only, not the keys).
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
principal_id: ID of the principal to list keys for.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of ApiKeyMetadata for the principal's keys.
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
@abstractmethod
|
|
216
|
+
def count_keys(self, principal_id: str) -> int:
|
|
217
|
+
"""Count active (non-revoked) API keys for a principal.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
principal_id: ID of the principal to count keys for.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Number of active API keys.
|
|
224
|
+
"""
|
|
225
|
+
...
|