aevum-server 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.
- aevum/server/__init__.py +7 -0
- aevum/server/__main__.py +33 -0
- aevum/server/app.py +165 -0
- aevum/server/core/__init__.py +1 -0
- aevum/server/core/config.py +29 -0
- aevum/server/core/deps.py +86 -0
- aevum/server/core/security.py +47 -0
- aevum/server/middleware.py +57 -0
- aevum/server/py.typed +0 -0
- aevum/server/routes/__init__.py +1 -0
- aevum/server/routes/admin.py +108 -0
- aevum/server/routes/data.py +80 -0
- aevum/server/routes/health.py +21 -0
- aevum/server/routes/replay.py +34 -0
- aevum/server/routes/review.py +69 -0
- aevum/server/schemas/__init__.py +1 -0
- aevum/server/schemas/requests.py +37 -0
- aevum/server/schemas/responses.py +29 -0
- aevum_server-0.2.0.dist-info/METADATA +35 -0
- aevum_server-0.2.0.dist-info/RECORD +21 -0
- aevum_server-0.2.0.dist-info/WHEEL +4 -0
aevum/server/__init__.py
ADDED
aevum/server/__main__.py
ADDED
|
@@ -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()
|
aevum/server/app.py
ADDED
|
@@ -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
|
aevum/server/py.typed
ADDED
|
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."}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
POST /v1/ingest, POST /v1/query, POST /v1/commit — public data API.
|
|
3
|
+
Spec Section 10.3.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from aevum.core.engine import Engine
|
|
11
|
+
from aevum.core.envelope.models import OutputEnvelope
|
|
12
|
+
from fastapi import APIRouter, Depends, Header
|
|
13
|
+
|
|
14
|
+
from aevum.server.core.deps import get_actor, get_correlation_id, get_engine
|
|
15
|
+
from aevum.server.schemas.requests import CommitRequest, IngestRequest, QueryRequest
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.post("/ingest", response_model=OutputEnvelope)
|
|
21
|
+
async def ingest(
|
|
22
|
+
body: IngestRequest,
|
|
23
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
24
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
25
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
26
|
+
idempotency_key: Annotated[str | None, Header()] = None,
|
|
27
|
+
) -> OutputEnvelope:
|
|
28
|
+
"""
|
|
29
|
+
Move data through the governed membrane into the knowledge graph.
|
|
30
|
+
Supports Idempotency-Key header for safe retries.
|
|
31
|
+
"""
|
|
32
|
+
return engine.ingest(
|
|
33
|
+
data=body.data,
|
|
34
|
+
provenance=body.provenance,
|
|
35
|
+
purpose=body.purpose,
|
|
36
|
+
subject_id=body.subject_id,
|
|
37
|
+
actor=actor,
|
|
38
|
+
idempotency_key=idempotency_key,
|
|
39
|
+
correlation_id=correlation_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.post("/query", response_model=OutputEnvelope)
|
|
44
|
+
async def query(
|
|
45
|
+
body: QueryRequest,
|
|
46
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
47
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
48
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
49
|
+
) -> OutputEnvelope:
|
|
50
|
+
"""Traverse the knowledge graph for a declared purpose."""
|
|
51
|
+
return engine.query(
|
|
52
|
+
purpose=body.purpose,
|
|
53
|
+
subject_ids=body.subject_ids,
|
|
54
|
+
actor=actor,
|
|
55
|
+
constraints=body.constraints,
|
|
56
|
+
classification_max=body.classification_max,
|
|
57
|
+
correlation_id=correlation_id,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.post("/commit", response_model=OutputEnvelope)
|
|
62
|
+
async def commit(
|
|
63
|
+
body: CommitRequest,
|
|
64
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
65
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
66
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
67
|
+
idempotency_key: Annotated[str | None, Header()] = None,
|
|
68
|
+
) -> OutputEnvelope:
|
|
69
|
+
"""
|
|
70
|
+
Append an event to the episodic ledger.
|
|
71
|
+
Supports Idempotency-Key header for safe retries.
|
|
72
|
+
event_type must not use kernel-reserved prefixes.
|
|
73
|
+
"""
|
|
74
|
+
return engine.commit(
|
|
75
|
+
event_type=body.event_type,
|
|
76
|
+
payload=body.payload,
|
|
77
|
+
actor=actor,
|
|
78
|
+
idempotency_key=idempotency_key,
|
|
79
|
+
correlation_id=correlation_id,
|
|
80
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GET /v1/health — liveness probe. No auth required. Spec Section 10.3.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from aevum.server.schemas.responses import HealthResponse
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/health", response_model=HealthResponse)
|
|
15
|
+
async def health() -> HealthResponse:
|
|
16
|
+
"""
|
|
17
|
+
Health check. MUST NOT require authentication.
|
|
18
|
+
Returns 200 when the server is accepting requests.
|
|
19
|
+
"""
|
|
20
|
+
from aevum.server import __version__
|
|
21
|
+
return HealthResponse(status="ok", version=__version__)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GET /v1/replay/{audit_id} — reconstruct a past decision. Spec Section 10.3.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
from aevum.core.engine import Engine
|
|
10
|
+
from aevum.core.envelope.models import OutputEnvelope
|
|
11
|
+
from fastapi import APIRouter, Depends
|
|
12
|
+
|
|
13
|
+
from aevum.server.core.deps import get_actor, get_correlation_id, get_engine
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/replay/{audit_id:path}", response_model=OutputEnvelope)
|
|
19
|
+
async def replay(
|
|
20
|
+
audit_id: str,
|
|
21
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
22
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
23
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
24
|
+
) -> OutputEnvelope:
|
|
25
|
+
"""
|
|
26
|
+
Reconstruct a past decision faithfully.
|
|
27
|
+
audit_id must be a valid urn:aevum:audit:* identifier.
|
|
28
|
+
Consent for the replay operation is checked by the kernel.
|
|
29
|
+
"""
|
|
30
|
+
return engine.replay(
|
|
31
|
+
audit_id=audit_id,
|
|
32
|
+
actor=actor,
|
|
33
|
+
correlation_id=correlation_id,
|
|
34
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GET /v1/review/{audit_id} — poll status
|
|
3
|
+
POST /v1/review/{audit_id}/approve — approve
|
|
4
|
+
POST /v1/review/{audit_id}/veto — veto
|
|
5
|
+
Spec Section 10.3.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
from aevum.core.engine import Engine
|
|
13
|
+
from aevum.core.envelope.models import OutputEnvelope
|
|
14
|
+
from fastapi import APIRouter, Depends
|
|
15
|
+
|
|
16
|
+
from aevum.server.core.deps import get_actor, get_correlation_id, get_engine
|
|
17
|
+
from aevum.server.schemas.requests import ReviewActionRequest
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/review/{audit_id:path}", response_model=OutputEnvelope)
|
|
23
|
+
async def get_review(
|
|
24
|
+
audit_id: str,
|
|
25
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
26
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
27
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
28
|
+
) -> OutputEnvelope:
|
|
29
|
+
"""Poll the status of a pending review."""
|
|
30
|
+
return engine.review(
|
|
31
|
+
audit_id=audit_id,
|
|
32
|
+
actor=actor,
|
|
33
|
+
action=None,
|
|
34
|
+
correlation_id=correlation_id,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/review/{audit_id:path}/approve", response_model=OutputEnvelope)
|
|
39
|
+
async def approve_review(
|
|
40
|
+
audit_id: str,
|
|
41
|
+
body: ReviewActionRequest,
|
|
42
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
43
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
44
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
45
|
+
) -> OutputEnvelope:
|
|
46
|
+
"""Record human approval of a pending review."""
|
|
47
|
+
return engine.review(
|
|
48
|
+
audit_id=audit_id,
|
|
49
|
+
actor=actor,
|
|
50
|
+
action="approve",
|
|
51
|
+
correlation_id=correlation_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("/review/{audit_id:path}/veto", response_model=OutputEnvelope)
|
|
56
|
+
async def veto_review(
|
|
57
|
+
audit_id: str,
|
|
58
|
+
body: ReviewActionRequest,
|
|
59
|
+
actor: Annotated[str, Depends(get_actor)],
|
|
60
|
+
engine: Annotated[Engine, Depends(get_engine)],
|
|
61
|
+
correlation_id: Annotated[str, Depends(get_correlation_id)],
|
|
62
|
+
) -> OutputEnvelope:
|
|
63
|
+
"""Record human veto of a pending review. Veto-as-default: silence = veto."""
|
|
64
|
+
return engine.review(
|
|
65
|
+
audit_id=audit_id,
|
|
66
|
+
actor=actor,
|
|
67
|
+
action="veto",
|
|
68
|
+
correlation_id=correlation_id,
|
|
69
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""aevum.server.schemas — HTTP request and response models."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP request schemas — Pydantic models for incoming JSON bodies.
|
|
3
|
+
These are HTTP wire types. They map to Engine function parameters.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IngestRequest(BaseModel):
|
|
14
|
+
model_config = ConfigDict(frozen=True)
|
|
15
|
+
data: dict[str, Any]
|
|
16
|
+
provenance: dict[str, Any]
|
|
17
|
+
purpose: str
|
|
18
|
+
subject_id: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueryRequest(BaseModel):
|
|
22
|
+
model_config = ConfigDict(frozen=True)
|
|
23
|
+
purpose: str
|
|
24
|
+
subject_ids: list[str]
|
|
25
|
+
constraints: dict[str, Any] | None = None
|
|
26
|
+
classification_max: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommitRequest(BaseModel):
|
|
30
|
+
model_config = ConfigDict(frozen=True)
|
|
31
|
+
event_type: str
|
|
32
|
+
payload: dict[str, Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReviewActionRequest(BaseModel):
|
|
36
|
+
"""Body for approve/veto — empty object required by spec."""
|
|
37
|
+
model_config = ConfigDict(frozen=True)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP response schemas.
|
|
3
|
+
Most routes return OutputEnvelope directly.
|
|
4
|
+
These are supplementary schemas for non-envelope responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HealthResponse(BaseModel):
|
|
15
|
+
"""GET /v1/health response."""
|
|
16
|
+
status: str
|
|
17
|
+
version: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProblemDetail(BaseModel):
|
|
21
|
+
"""RFC 9457 Problem Details. Content-Type: application/problem+json."""
|
|
22
|
+
type: str
|
|
23
|
+
title: str
|
|
24
|
+
status: int
|
|
25
|
+
detail: str
|
|
26
|
+
instance: str | None = None
|
|
27
|
+
request_id: str | None = None
|
|
28
|
+
audit_id: str | None = None
|
|
29
|
+
extensions: dict[str, Any] | None = None
|
|
@@ -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,21 @@
|
|
|
1
|
+
aevum/server/__init__.py,sha256=Ic4YSDLfma20yFYi_nVCJAU2XzweZzqW7Se85SWuYPs,163
|
|
2
|
+
aevum/server/__main__.py,sha256=qSxPrgnwe854AOJNED7MVRlnjdFwpwrNJMX_8ABnlk0,932
|
|
3
|
+
aevum/server/app.py,sha256=GVcMsFNk44txAjeHwQtrkrvKVHYywTks66k5hf2qU2k,5604
|
|
4
|
+
aevum/server/middleware.py,sha256=jfZhQTXo_ygnvhTaTDHdYxtBPFVm-RKcKgzfzDgI4wQ,2028
|
|
5
|
+
aevum/server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
aevum/server/core/__init__.py,sha256=79erO6dIZMSOn_sJrvWH4KTGK_8SaRbJJeEZvRI3Z_A,68
|
|
7
|
+
aevum/server/core/config.py,sha256=A9_b5oLFz6iVUTUPXdevz0wEPJ1KA93pEU2zHHoL7o0,769
|
|
8
|
+
aevum/server/core/deps.py,sha256=oN-A7tL8dwS0T2xA2yuWSEznh3Oa_9lne3sZ-JXwQBA,3195
|
|
9
|
+
aevum/server/core/security.py,sha256=tg1uPfudWTg44jk0tjiFtT6Lo87T5tVFQ9XKnKfW_5k,1572
|
|
10
|
+
aevum/server/routes/__init__.py,sha256=R0WOdc6XjT8r6ljwNEJcLosmHt_C0Xm9XMi7T1khLdo,51
|
|
11
|
+
aevum/server/routes/admin.py,sha256=uiM0LzVwjSpYJgUBw3bjBzIKvNlZLV4TwA0iRbGneM4,3657
|
|
12
|
+
aevum/server/routes/data.py,sha256=ExjKfxc7IWAYCya7UcgsjdUisTFS1259iRrSJQaQ_ro,2619
|
|
13
|
+
aevum/server/routes/health.py,sha256=OHR4VZUcZKUJmb3B-vmbzJM7M4kv1diSF8ANSB_uyoE,566
|
|
14
|
+
aevum/server/routes/replay.py,sha256=Zv-lL9GyFLUVeTmI0XIu402JVeu_lFfWmGR1R9cvQ_c,1012
|
|
15
|
+
aevum/server/routes/review.py,sha256=12t-Om6FHN0g4v5M3wqBz5SkaIvAdQvyF-ABZvO1ePs,2170
|
|
16
|
+
aevum/server/schemas/__init__.py,sha256=lhl_rUmB1Bb3UCvvrMfxs9SH5r-sqfu5U4eeSZIk5CI,66
|
|
17
|
+
aevum/server/schemas/requests.py,sha256=8duYkMsi8HqOqGJ2qOdN5IfbqtXmIIgWAMFXQ4rlzRA,926
|
|
18
|
+
aevum/server/schemas/responses.py,sha256=H8fmS_UGp8L6CuYmwHuZ5_3G8bj6xSVQTcxMocCURa8,673
|
|
19
|
+
aevum_server-0.2.0.dist-info/METADATA,sha256=yRCAVrxxIsD2h3fRyiL9rrYcIjoYg7jveWdHr6U1nMs,1254
|
|
20
|
+
aevum_server-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
21
|
+
aevum_server-0.2.0.dist-info/RECORD,,
|