feat: strengthen module contracts and shared WebUI runtime

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent 53e947935a
commit 68328f3d8e
57 changed files with 4358 additions and 371 deletions
+12 -1
View File
@@ -72,8 +72,19 @@ def _validate_production_startup() -> None:
"off",
}
if throttle_enabled and not os.getenv("REDIS_URL", "").strip():
allow_process_local = os.getenv(
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE",
"false",
).strip().lower() in {"1", "true", "yes", "on"}
if not allow_process_local:
raise RuntimeError(
"Production login throttling requires REDIS_URL. "
"Set GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE=true only "
"for an explicitly single-process deployment."
)
logger.warning(
"Redis is not configured; login throttling is process-local and will not coordinate horizontally"
"Redis is not configured; login throttling is explicitly limited "
"to this process"
)
+14 -4
View File
@@ -1,10 +1,11 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.exc import SQLAlchemyError
from govoplan_core.admin.models import SystemSettings
from govoplan_core.admin.settings import SYSTEM_SETTINGS_ID
from govoplan_core.auth import ApiPrincipal, get_api_principal
from govoplan_core.core.maintenance import saved_maintenance_mode
from govoplan_core.core.modules import FrontendModule, FrontendRoute, ModuleManifest, NavItem, PublicFrontendRoute
from govoplan_core.core.registry import PlatformRegistry, manifest_view_surfaces
@@ -153,7 +154,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
}
@router.get("/modules")
def modules(request: Request):
def modules(
request: Request,
_principal: ApiPrincipal = Depends(get_api_principal),
):
registry = _registry(request)
return {
"modules": [
@@ -190,7 +194,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
}
@router.get("/permissions")
def permissions(request: Request):
def permissions(
request: Request,
_principal: ApiPrincipal = Depends(get_api_principal),
):
registry = _registry(request)
return {
"permissions": [
@@ -210,7 +217,10 @@ def create_platform_router(settings: object | None = None) -> APIRouter:
}
@router.get("/navigation")
def navigation(request: Request):
def navigation(
request: Request,
_principal: ApiPrincipal = Depends(get_api_principal),
):
registry = _registry(request)
return {
"items": [_nav_item_payload(item) for item in registry.nav_items()]
+58 -10
View File
@@ -3,6 +3,9 @@ from __future__ import annotations
from collections.abc import Callable, Iterable, Sequence
import importlib
from govoplan_core.core.access import (
DEFAULT_CAPABILITY_PROVIDERS,
)
from govoplan_core.core.discovery import discover_module_manifests
from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.registry import PlatformRegistry, RegistryError
@@ -15,7 +18,6 @@ _BUILTIN_MANIFESTS = {
"tenancy": "govoplan_tenancy.backend.manifest:get_manifest",
}
def parse_enabled_modules(value: str | Iterable[str]) -> list[str]:
if isinstance(value, str):
items = [item.strip() for item in value.split(",")]
@@ -57,22 +59,68 @@ def available_module_manifests(
return manifests
def build_platform_registry(enabled_modules: str | Iterable[str], *, manifest_factories: Sequence[ManifestFactory] = ()) -> PlatformRegistry:
def build_platform_registry(
enabled_modules: str | Iterable[str],
*,
manifest_factories: Sequence[ManifestFactory] = (),
) -> PlatformRegistry:
requested = parse_enabled_modules(enabled_modules)
if "access" not in requested:
requested.insert(0, "access")
available = available_module_manifests(manifest_factories, enabled_modules=requested)
available = _resolve_required_manifests(
requested,
manifest_factories=manifest_factories,
)
registry = PlatformRegistry()
for module_id in requested:
manifest = available.get(module_id)
if manifest is None:
raise RegistryError(f"Enabled module is not available: {module_id}")
for module_id, manifest in available.items():
registry.register(manifest)
registry.validate()
return registry
def _resolve_required_manifests(
requested: Sequence[str],
*,
manifest_factories: Sequence[ManifestFactory],
) -> dict[str, ModuleManifest]:
manifests: dict[str, ModuleManifest] = {}
pending = list(dict.fromkeys(requested))
while pending:
module_ids = [module_id for module_id in pending if module_id not in manifests]
pending = []
if not module_ids:
break
available = available_module_manifests(
manifest_factories,
enabled_modules=module_ids,
)
for module_id in module_ids:
manifest = available.get(module_id)
if manifest is None:
raise RegistryError(f"Enabled module is not available: {module_id}")
manifests[module_id] = manifest
provided_capabilities = {
capability
for manifest in manifests.values()
for capability in manifest.capability_factories
}
for manifest in tuple(manifests.values()):
pending.extend(
dependency_id
for dependency_id in manifest.dependencies
if dependency_id not in manifests
)
for capability in manifest.required_capabilities:
if capability in provided_capabilities:
continue
provider_id = DEFAULT_CAPABILITY_PROVIDERS.get(capability)
if provider_id is not None and provider_id not in manifests:
pending.append(provider_id)
return manifests
def _load_builtin_manifest(module_id: str) -> ModuleManifest:
target = _BUILTIN_MANIFESTS[module_id]
module_name, function_name = validate_object_path(target)