aevum-server 0.2.0__tar.gz

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 (28) hide show
  1. aevum_server-0.2.0/.gitignore +31 -0
  2. aevum_server-0.2.0/PKG-INFO +35 -0
  3. aevum_server-0.2.0/README.md +11 -0
  4. aevum_server-0.2.0/pyproject.toml +59 -0
  5. aevum_server-0.2.0/src/aevum/server/__init__.py +7 -0
  6. aevum_server-0.2.0/src/aevum/server/__main__.py +33 -0
  7. aevum_server-0.2.0/src/aevum/server/app.py +165 -0
  8. aevum_server-0.2.0/src/aevum/server/core/__init__.py +1 -0
  9. aevum_server-0.2.0/src/aevum/server/core/config.py +29 -0
  10. aevum_server-0.2.0/src/aevum/server/core/deps.py +86 -0
  11. aevum_server-0.2.0/src/aevum/server/core/security.py +47 -0
  12. aevum_server-0.2.0/src/aevum/server/middleware.py +57 -0
  13. aevum_server-0.2.0/src/aevum/server/py.typed +0 -0
  14. aevum_server-0.2.0/src/aevum/server/routes/__init__.py +1 -0
  15. aevum_server-0.2.0/src/aevum/server/routes/admin.py +108 -0
  16. aevum_server-0.2.0/src/aevum/server/routes/data.py +80 -0
  17. aevum_server-0.2.0/src/aevum/server/routes/health.py +21 -0
  18. aevum_server-0.2.0/src/aevum/server/routes/replay.py +34 -0
  19. aevum_server-0.2.0/src/aevum/server/routes/review.py +69 -0
  20. aevum_server-0.2.0/src/aevum/server/schemas/__init__.py +1 -0
  21. aevum_server-0.2.0/src/aevum/server/schemas/requests.py +37 -0
  22. aevum_server-0.2.0/src/aevum/server/schemas/responses.py +29 -0
  23. aevum_server-0.2.0/tests/conftest.py +68 -0
  24. aevum_server-0.2.0/tests/test_auth.py +40 -0
  25. aevum_server-0.2.0/tests/test_data_routes.py +159 -0
  26. aevum_server-0.2.0/tests/test_health.py +37 -0
  27. aevum_server-0.2.0/tests/test_middleware.py +37 -0
  28. aevum_server-0.2.0/tests/test_oidc_auth.py +160 -0
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+
13
+ # Tools
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+ .hypothesis/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Verify scripts (run locally, never commit)
30
+ verify_phase*.py
31
+ scripts/verify_phase*.py
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: aevum-server
3
+ Version: 0.2.0
4
+ Summary: Aevum HTTP API server — wraps the five functions for any consumer.
5
+ Project-URL: Homepage, https://aevum.build
6
+ Project-URL: Repository, https://github.com/aevum-labs/aevum
7
+ License: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: aevum-core
17
+ Requires-Dist: fastapi<0.136,>=0.115
18
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.50b0
19
+ Requires-Dist: opentelemetry-sdk>=1.25
20
+ Requires-Dist: pydantic-settings>=2.0
21
+ Requires-Dist: python-dotenv>=1.2.2
22
+ Requires-Dist: uvicorn[standard]>=0.30
23
+ Description-Content-Type: text/markdown
24
+
25
+ # aevum-server
26
+
27
+ FastAPI HTTP server wrapping the five governed Aevum kernel functions (`ingest`, `query`, `review`, `commit`, `replay`).
28
+
29
+ ```bash
30
+ pip install aevum-server
31
+ aevum server start --graph memory
32
+ ```
33
+
34
+ See the [main repository README](https://github.com/aevum-labs/aevum) for configuration and deployment options.
35
+
@@ -0,0 +1,11 @@
1
+ # aevum-server
2
+
3
+ FastAPI HTTP server wrapping the five governed Aevum kernel functions (`ingest`, `query`, `review`, `commit`, `replay`).
4
+
5
+ ```bash
6
+ pip install aevum-server
7
+ aevum server start --graph memory
8
+ ```
9
+
10
+ See the [main repository README](https://github.com/aevum-labs/aevum) for configuration and deployment options.
11
+
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "aevum-server"
3
+ version = "0.2.0"
4
+ description = "Aevum HTTP API server — wraps the five functions for any consumer."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "Apache-2.0" }
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = [
18
+ "aevum-core",
19
+ "fastapi>=0.115,<0.136",
20
+ "uvicorn[standard]>=0.30",
21
+ "pydantic-settings>=2.0",
22
+ "opentelemetry-instrumentation-fastapi>=0.50b0",
23
+ "opentelemetry-sdk>=1.25",
24
+ "python-dotenv>=1.2.2", # CVE-2026-28684 fixed in 1.2.2
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://aevum.build"
29
+ Repository = "https://github.com/aevum-labs/aevum"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/aevum"]
37
+
38
+ [tool.uv.sources]
39
+ aevum-core = { workspace = true }
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ asyncio_mode = "auto"
44
+ addopts = "--tb=short"
45
+ pythonpath = ["src", "tests"]
46
+
47
+ [tool.mypy]
48
+ strict = true
49
+ python_version = "3.11"
50
+ mypy_path = "src"
51
+ explicit_package_bases = true
52
+ ignore_missing_imports = true
53
+
54
+ [tool.ruff]
55
+ line-length = 130
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
59
+ ignore = ["ANN401"]
@@ -0,0 +1,7 @@
1
+ """aevum.server — HTTP API server wrapping the Aevum kernel."""
2
+
3
+ from aevum.server.app import create_app
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["create_app"]
@@ -0,0 +1,33 @@
1
+ """
2
+ python -m aevum.server — development convenience entrypoint.
3
+
4
+ Starts aevum-server with an in-memory Engine and default settings.
5
+ NOT for production. Production operators use:
6
+ uvicorn aevum.server.app:create_app --factory
7
+ gunicorn aevum.server.app:create_app -k uvicorn.workers.UvicornWorker
8
+
9
+ Configuration via environment variables (see aevum/server/core/config.py).
10
+ For full CLI control use the aevum-cli package: `aevum server start`.
11
+ """
12
+
13
+ import uvicorn
14
+ from aevum.core.engine import Engine
15
+
16
+ from aevum.server.app import create_app
17
+ from aevum.server.core.config import Settings
18
+
19
+
20
+ def main() -> None:
21
+ settings = Settings()
22
+ engine = Engine(opa_url=settings.opa_url or None)
23
+ app = create_app(engine, settings=settings)
24
+ uvicorn.run(
25
+ app,
26
+ host=settings.host,
27
+ port=settings.port,
28
+ log_level="info",
29
+ )
30
+
31
+
32
+ if __name__ == "__main__":
33
+ main()
@@ -0,0 +1,165 @@
1
+ """
2
+ create_app — FastAPI application factory.
3
+
4
+ Usage:
5
+ from aevum.core import Engine
6
+ from aevum.server.app import create_app
7
+
8
+ engine = Engine()
9
+ app = create_app(engine)
10
+
11
+ # Production:
12
+ # uvicorn aevum.server.app:create_app --factory
13
+ # gunicorn aevum.server.app:create_app -k uvicorn.workers.UvicornWorker
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import Any
20
+
21
+ from aevum.core.engine import Engine
22
+ from aevum.core.exceptions import (
23
+ AevumError,
24
+ BarrierViolationError,
25
+ ConsentRequiredError,
26
+ ProvenanceRequiredError,
27
+ ReplayNotFoundError,
28
+ ReviewAlreadyResolvedError,
29
+ ReviewNotFoundError,
30
+ )
31
+ from fastapi import FastAPI, Request
32
+ from fastapi.responses import JSONResponse
33
+
34
+ from aevum.server.core.config import Settings
35
+ from aevum.server.middleware import AevumMiddleware
36
+ from aevum.server.routes import admin, data, health, replay, review
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # RFC 9457 problem type → HTTP status code mapping
41
+ _PROBLEM_STATUS: dict[type[AevumError], int] = {
42
+ ConsentRequiredError: 403,
43
+ ProvenanceRequiredError: 400,
44
+ ReplayNotFoundError: 404,
45
+ ReviewNotFoundError: 404,
46
+ ReviewAlreadyResolvedError: 409,
47
+ BarrierViolationError: 403,
48
+ }
49
+
50
+ _PROBLEM_TYPES: dict[type[AevumError], str] = {
51
+ ConsentRequiredError: "consent-required",
52
+ ProvenanceRequiredError: "validation-error",
53
+ ReplayNotFoundError: "replay-not-found",
54
+ ReviewNotFoundError: "review-not-found",
55
+ ReviewAlreadyResolvedError: "review-already-resolved",
56
+ BarrierViolationError: "barrier-triggered",
57
+ }
58
+
59
+
60
+ def _problem_response(
61
+ exc: AevumError,
62
+ status: int,
63
+ problem_type: str,
64
+ request_id: str | None = None,
65
+ ) -> JSONResponse:
66
+ """Return RFC 9457 application/problem+json response."""
67
+ body: dict[str, Any] = {
68
+ "type": f"https://aevum.build/problems/{problem_type}",
69
+ "title": exc.__class__.__name__,
70
+ "status": status,
71
+ "detail": str(exc),
72
+ }
73
+ if request_id:
74
+ body["request_id"] = request_id
75
+ return JSONResponse(
76
+ content=body,
77
+ status_code=status,
78
+ media_type="application/problem+json",
79
+ )
80
+
81
+
82
+ def create_app(
83
+ engine: Engine | None = None,
84
+ settings: Settings | None = None,
85
+ ) -> FastAPI:
86
+ """
87
+ Create the Aevum HTTP API FastAPI application.
88
+
89
+ Args:
90
+ engine: Aevum kernel instance. Creates Engine() with defaults if None.
91
+ settings: Server settings. Reads from environment if None.
92
+
93
+ Returns:
94
+ Configured FastAPI application.
95
+ """
96
+ if engine is None:
97
+ engine = Engine()
98
+ if settings is None:
99
+ settings = Settings()
100
+
101
+ app = FastAPI(
102
+ title="Aevum HTTP API",
103
+ version="0.2.0",
104
+ description="Replay-first, policy-governed context kernel — HTTP surface.",
105
+ docs_url="/v1/docs",
106
+ openapi_url="/v1/openapi.json",
107
+ redoc_url=None,
108
+ )
109
+
110
+ # Store engine and settings on app state for dependency injection
111
+ app.state.engine = engine
112
+ app.state.settings = settings
113
+
114
+ # Middleware (order matters: outermost runs first on request, last on response)
115
+ app.add_middleware(
116
+ AevumMiddleware,
117
+ rate_limit_per_minute=settings.rate_limit_per_minute,
118
+ )
119
+
120
+ # OTel instrumentation (sanitize X-Aevum-Key from spans)
121
+ if settings.otel_enabled:
122
+ try:
123
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
124
+ FastAPIInstrumentor.instrument_app(
125
+ app,
126
+ # Sanitize auth header — must NOT appear in trace exports
127
+ excluded_urls="",
128
+ http_capture_headers_server_request=["x-request-id"],
129
+ http_capture_headers_server_response=["x-request-id", "x-response-time-ms"],
130
+ )
131
+ logger.info("OTel FastAPI instrumentation enabled")
132
+ except ImportError:
133
+ logger.warning("opentelemetry-instrumentation-fastapi not available")
134
+
135
+ # Global exception handler — RFC 9457 format
136
+ @app.exception_handler(AevumError)
137
+ async def aevum_error_handler(request: Request, exc: AevumError) -> JSONResponse:
138
+ request_id = request.headers.get("x-request-id")
139
+ status = _PROBLEM_STATUS.get(type(exc), 500)
140
+ problem_type = _PROBLEM_TYPES.get(type(exc), "internal-error")
141
+ return _problem_response(exc, status, problem_type, request_id)
142
+
143
+ @app.exception_handler(Exception)
144
+ async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
145
+ # Never expose internal details to callers
146
+ logger.exception("Unhandled exception in request %s %s", request.method, request.url.path)
147
+ request_id = request.headers.get("x-request-id")
148
+ body: dict[str, Any] = {
149
+ "type": "https://aevum.build/problems/internal-error",
150
+ "title": "Internal Server Error",
151
+ "status": 500,
152
+ "detail": "An unexpected error occurred.",
153
+ }
154
+ if request_id:
155
+ body["request_id"] = request_id
156
+ return JSONResponse(content=body, status_code=500, media_type="application/problem+json")
157
+
158
+ # Routes
159
+ app.include_router(health.router, prefix="/v1")
160
+ app.include_router(data.router, prefix="/v1")
161
+ app.include_router(replay.router, prefix="/v1")
162
+ app.include_router(review.router, prefix="/v1")
163
+ app.include_router(admin.router, prefix="/_aevum/v1")
164
+
165
+ return app
@@ -0,0 +1 @@
1
+ """aevum.server.core — config, security, shared dependencies."""
@@ -0,0 +1,29 @@
1
+ """
2
+ Server configuration via environment variables.
3
+ All settings have safe defaults for development.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ class Settings(BaseSettings):
12
+ model_config = SettingsConfigDict(env_prefix="AEVUM_", case_sensitive=False)
13
+
14
+ # Network
15
+ host: str = "0.0.0.0"
16
+ port: int = 8000
17
+
18
+ # Auth
19
+ api_key: str = "dev-insecure-key-change-in-production"
20
+
21
+ # Policy (optional — permissive stub if not set)
22
+ opa_url: str = ""
23
+
24
+ # Rate limiting (headers always present; enforcement is operator responsibility)
25
+ rate_limit_per_minute: int = 1000
26
+
27
+ # OTel
28
+ otel_enabled: bool = False
29
+ otel_service_name: str = "aevum-server"
@@ -0,0 +1,86 @@
1
+ """
2
+ FastAPI shared dependencies.
3
+ Injected via Depends() into route handlers.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated, Any
9
+
10
+ from aevum.core.engine import Engine
11
+ from fastapi import Header, HTTPException, Request, status
12
+
13
+ from aevum.server.core.config import Settings
14
+ from aevum.server.core.security import require_api_key
15
+
16
+
17
+ def get_engine(request: Request) -> Engine:
18
+ """Extract Engine from app state (set in create_app)."""
19
+ return request.app.state.engine # type: ignore[no-any-return]
20
+
21
+
22
+ def get_settings(request: Request) -> Settings:
23
+ """Extract Settings from app state."""
24
+ return request.app.state.settings # type: ignore[no-any-return]
25
+
26
+
27
+ async def get_actor(
28
+ request: Request,
29
+ x_aevum_key: Annotated[str | None, Header()] = None,
30
+ authorization: Annotated[str | None, Header()] = None,
31
+ ) -> str:
32
+ """
33
+ Validate auth and return actor identity.
34
+
35
+ Precedence:
36
+ 1. Authorization: Bearer <token> → resolved via installed OIDC complication
37
+ (fail-closed: rejects Bearer if no OIDC complication is active)
38
+ 2. X-Aevum-Key header → API key validation
39
+
40
+ The actor string returned is used throughout the kernel as the principal
41
+ identity for consent and audit purposes.
42
+ """
43
+ settings: Settings = request.app.state.settings
44
+ engine: Engine = request.app.state.engine
45
+
46
+ if authorization and authorization.lower().startswith("bearer "):
47
+ token = authorization[7:].strip()
48
+ oidc_comp = engine.get_active_complication_by_capability("oidc-validation")
49
+ if oidc_comp is None:
50
+ raise HTTPException(
51
+ status_code=status.HTTP_401_UNAUTHORIZED,
52
+ detail={
53
+ "type": "https://aevum.build/problems/authentication-required",
54
+ "title": "Authentication Required",
55
+ "status": 401,
56
+ "detail": "Bearer token presented but no OIDC complication is active.",
57
+ },
58
+ headers={"Content-Type": "application/problem+json"},
59
+ )
60
+ result: dict[str, Any] = await oidc_comp.run(
61
+ {"metadata": {"bearer_token": token}}, {}
62
+ )
63
+ if not result.get("oidc_validated"):
64
+ raise HTTPException(
65
+ status_code=status.HTTP_401_UNAUTHORIZED,
66
+ detail={
67
+ "type": "https://aevum.build/problems/authentication-required",
68
+ "title": "Authentication Required",
69
+ "status": 401,
70
+ "detail": result.get("reason", "Bearer token validation failed."),
71
+ },
72
+ headers={"Content-Type": "application/problem+json"},
73
+ )
74
+ actor: str = result["resolved_actor"]
75
+ return actor
76
+
77
+ return require_api_key(x_aevum_key, settings.api_key)
78
+
79
+
80
+ def get_correlation_id(
81
+ request: Request,
82
+ x_request_id: Annotated[str | None, Header()] = None,
83
+ ) -> str:
84
+ """Return client-provided or server-generated correlation ID."""
85
+ import uuid
86
+ return x_request_id or str(uuid.uuid4())
@@ -0,0 +1,47 @@
1
+ """
2
+ Authentication — X-Aevum-Key header validation.
3
+
4
+ API key authentication only. For OIDC bearer token support install
5
+ the aevum-oidc complication and add it to the Engine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from fastapi import HTTPException, status
11
+ from fastapi.security import APIKeyHeader
12
+
13
+ _API_KEY_HEADER = APIKeyHeader(name="X-Aevum-Key", auto_error=False)
14
+
15
+
16
+ def require_api_key(
17
+ api_key_value: str | None,
18
+ configured_key: str,
19
+ ) -> str:
20
+ """
21
+ Validate the X-Aevum-Key header.
22
+ Returns the key value (used as actor identity) if valid.
23
+ Raises 401 if absent or invalid.
24
+ """
25
+ if not api_key_value:
26
+ raise HTTPException(
27
+ status_code=status.HTTP_401_UNAUTHORIZED,
28
+ detail={
29
+ "type": "https://aevum.build/problems/authentication-required",
30
+ "title": "Authentication Required",
31
+ "status": 401,
32
+ "detail": "X-Aevum-Key header is required.",
33
+ },
34
+ headers={"Content-Type": "application/problem+json"},
35
+ )
36
+ if api_key_value != configured_key:
37
+ raise HTTPException(
38
+ status_code=status.HTTP_401_UNAUTHORIZED,
39
+ detail={
40
+ "type": "https://aevum.build/problems/authentication-required",
41
+ "title": "Authentication Required",
42
+ "status": 401,
43
+ "detail": "Invalid API key.",
44
+ },
45
+ headers={"Content-Type": "application/problem+json"},
46
+ )
47
+ return api_key_value
@@ -0,0 +1,57 @@
1
+ """
2
+ Middleware — correlation IDs, security headers, rate limit headers.
3
+ Applied to every response via app.middleware("http").
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ import uuid
10
+ from collections.abc import Awaitable, Callable
11
+
12
+ from fastapi import Request, Response
13
+ from starlette.middleware.base import BaseHTTPMiddleware
14
+ from starlette.types import ASGIApp
15
+
16
+
17
+ class AevumMiddleware(BaseHTTPMiddleware):
18
+ """
19
+ Single middleware handling:
20
+ 1. Correlation ID propagation (X-Request-ID)
21
+ 2. Security headers on every response
22
+ 3. Rate limit info headers (headers always present; enforcement is operator concern)
23
+ 4. Request timing
24
+ """
25
+
26
+ def __init__(self, app: ASGIApp, rate_limit_per_minute: int = 1000) -> None:
27
+ super().__init__(app)
28
+ self._rate_limit = rate_limit_per_minute
29
+
30
+ async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
31
+ start = time.perf_counter()
32
+
33
+ # Correlation ID
34
+ correlation_id = request.headers.get("x-request-id") or str(uuid.uuid4())
35
+
36
+ response: Response = await call_next(request)
37
+
38
+ duration_ms = int((time.perf_counter() - start) * 1000)
39
+
40
+ # Correlation ID echo
41
+ response.headers["X-Request-ID"] = correlation_id
42
+
43
+ # Security headers (spec Section 10.8)
44
+ response.headers["X-Content-Type-Options"] = "nosniff"
45
+ response.headers["X-Frame-Options"] = "DENY"
46
+ response.headers["Strict-Transport-Security"] = "max-age=31536000"
47
+ response.headers["Content-Security-Policy"] = "default-src 'none'"
48
+
49
+ # Rate limit info headers (spec Section 10.7)
50
+ response.headers["X-RateLimit-Limit"] = str(self._rate_limit)
51
+ response.headers["X-RateLimit-Remaining"] = str(self._rate_limit)
52
+ response.headers["X-RateLimit-Reset"] = "60"
53
+
54
+ # Timing
55
+ response.headers["X-Response-Time-Ms"] = str(duration_ms)
56
+
57
+ return response
File without changes
@@ -0,0 +1 @@
1
+ """aevum.server.routes — All route handlers."""
@@ -0,0 +1,108 @@
1
+ """
2
+ /_aevum/v1/* — admin API for complication lifecycle and server diagnostics.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Annotated, Any
8
+
9
+ from aevum.core.engine import Engine
10
+ from aevum.core.exceptions import ComplicationError
11
+ from fastapi import APIRouter, Depends, HTTPException, status
12
+ from pydantic import BaseModel
13
+
14
+ from aevum.server.core.deps import get_actor, get_engine
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ class ApproveRequest(BaseModel):
20
+ pass
21
+
22
+
23
+ class SuspendRequest(BaseModel):
24
+ pass
25
+
26
+
27
+ @router.get("/complications")
28
+ async def list_complications(
29
+ actor: Annotated[str, Depends(get_actor)],
30
+ engine: Annotated[Engine, Depends(get_engine)],
31
+ ) -> dict[str, Any]:
32
+ """List all complications with their current lifecycle state."""
33
+ return {"complications": engine.list_complications()}
34
+
35
+
36
+ @router.post("/complications/{complication_id}/approve")
37
+ async def approve_complication(
38
+ complication_id: str,
39
+ body: ApproveRequest,
40
+ actor: Annotated[str, Depends(get_actor)],
41
+ engine: Annotated[Engine, Depends(get_engine)],
42
+ ) -> dict[str, Any]:
43
+ """Approve a PENDING complication → ACTIVE."""
44
+ try:
45
+ engine.approve_complication(complication_id)
46
+ return {"approved": True, "name": complication_id}
47
+ except ComplicationError as e:
48
+ raise HTTPException(
49
+ status_code=status.HTTP_409_CONFLICT,
50
+ detail={"type": "https://aevum.build/problems/complication-error",
51
+ "title": "Complication Error", "status": 409, "detail": str(e)},
52
+ ) from e
53
+
54
+
55
+ @router.post("/complications/{complication_id}/suspend")
56
+ async def suspend_complication(
57
+ complication_id: str,
58
+ body: SuspendRequest,
59
+ actor: Annotated[str, Depends(get_actor)],
60
+ engine: Annotated[Engine, Depends(get_engine)],
61
+ ) -> dict[str, Any]:
62
+ """Suspend an ACTIVE complication."""
63
+ try:
64
+ engine.suspend_complication(complication_id)
65
+ return {"suspended": True, "name": complication_id}
66
+ except ComplicationError as e:
67
+ raise HTTPException(
68
+ status_code=status.HTTP_409_CONFLICT,
69
+ detail={"type": "https://aevum.build/problems/complication-error",
70
+ "title": "Complication Error", "status": 409, "detail": str(e)},
71
+ ) from e
72
+
73
+
74
+ @router.get("/complications/{complication_id}/health")
75
+ async def complication_health(
76
+ complication_id: str,
77
+ actor: Annotated[str, Depends(get_actor)],
78
+ engine: Annotated[Engine, Depends(get_engine)],
79
+ ) -> dict[str, Any]:
80
+ """Check health of a specific complication."""
81
+ complications = engine.list_complications()
82
+ if complication_id not in complications:
83
+ raise HTTPException(
84
+ status_code=status.HTTP_404_NOT_FOUND,
85
+ detail={"type": "https://aevum.build/problems/complication-not-found",
86
+ "title": "Not Found", "status": 404,
87
+ "detail": f"Complication '{complication_id}' not found"},
88
+ )
89
+ entry = complications[complication_id]
90
+ return {
91
+ "name": complication_id,
92
+ "state": entry["state"],
93
+ "healthy": entry["state"] == "ACTIVE",
94
+ }
95
+
96
+
97
+ @router.get("/usage")
98
+ async def get_usage(
99
+ actor: Annotated[str, Depends(get_actor)],
100
+ ) -> dict[str, Any]:
101
+ return {"usage": {}, "note": "Install a complication to see usage metrics here."}
102
+
103
+
104
+ @router.get("/federation/peers")
105
+ async def list_federation_peers(
106
+ actor: Annotated[str, Depends(get_actor)],
107
+ ) -> dict[str, Any]:
108
+ return {"peers": [], "note": "Install a complication to see it listed here."}