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.
@@ -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()
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any