diff --git a/FARM/FARM/settings.py b/FARM/FARM/settings.py index 7d986d5e..32018461 100644 --- a/FARM/FARM/settings.py +++ b/FARM/FARM/settings.py @@ -25,10 +25,26 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-insecure-key-change-me") + +def env_bool(name: str, default: bool = False) -> bool: + """Parse boolean-like environment variables safely.""" + value = os.environ.get(name) + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = env_bool("DJANGO_DEBUG", default=False) + +ALLOWED_HOSTS = [ + host.strip() + for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") + if host.strip() +] -ALLOWED_HOSTS = [] +if not DEBUG and SECRET_KEY == "dev-insecure-key-change-me": + msg = "DJANGO_SECRET_KEY must be set in non-debug environments." + raise RuntimeError(msg) # Application definition @@ -116,3 +132,13 @@ # https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = "static/" + + +# Basic production security hardening. These can be tuned via environment. +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" +SECURE_REFERRER_POLICY = "same-origin" +CSRF_COOKIE_SECURE = env_bool("DJANGO_CSRF_COOKIE_SECURE", default=not DEBUG) +SESSION_COOKIE_SECURE = env_bool("DJANGO_SESSION_COOKIE_SECURE", default=not DEBUG) +SECURE_SSL_REDIRECT = env_bool("DJANGO_SECURE_SSL_REDIRECT", default=not DEBUG) diff --git a/MAGE/api/main.py b/MAGE/api/main.py index a0e8844d..cf3a1bfd 100644 --- a/MAGE/api/main.py +++ b/MAGE/api/main.py @@ -7,6 +7,8 @@ from fastapi import FastAPI from fastmcp import FastMCP +from MAGE.api.services.encoders import list_encoders +from MAGE.api.services.encoders import service_app as encoders_service from MAGE.api.services.libraries import libraries from MAGE.api.services.libraries import service_app as libraries_service from MAGE.api.services.models import ( @@ -44,6 +46,7 @@ async def read_root() -> dict[str, str]: # Backward-compatible routes served directly by the gateway. api.add_api_route("/libraries", libraries, methods=["GET"], tags=["libraries"]) +api.add_api_route("/encoders", list_encoders, methods=["GET"], tags=["encoders"]) api.add_api_route("/model", model_list, methods=["GET"], tags=["models"]) api.add_api_route( "/model/{model_id}/is_pretrained", @@ -73,6 +76,7 @@ async def read_root() -> dict[str, str]: # Microservice mounts (service-style boundaries under dedicated prefixes). api.mount("/services/libraries", libraries_service) +api.mount("/services/encoders", encoders_service) api.mount("/services/models", models_service) api.mount("/services/modules", modules_service) diff --git a/MAGE/api/services/encoders.py b/MAGE/api/services/encoders.py new file mode 100644 index 00000000..2c1ba4eb --- /dev/null +++ b/MAGE/api/services/encoders.py @@ -0,0 +1,40 @@ +# Copyright (C) 2026 AIMER contributors. + +"""Encoders service: inspect SMP encoders and TIMM-backed support.""" + +from __future__ import annotations + +from importlib import import_module + +from fastapi import APIRouter, FastAPI, HTTPException + +router = APIRouter(tags=["encoders"]) + + +@router.get("/health") +async def healthcheck() -> dict[str, str]: + """Service-local health-check endpoint.""" + return {"encoders_service": "UP"} + + +@router.get("/encoders") +async def list_encoders() -> dict[str, list[str] | int]: + """List SMP encoders and a subset that are TIMM-backed.""" + try: + smp = import_module("segmentation_models_pytorch") + smp_encoders = smp.encoders + except Exception as exc: + raise HTTPException(status_code=503, detail="segmentation_models_pytorch is unavailable") from exc + + names = sorted(smp_encoders.get_encoder_names()) + timm_backed = [name for name in names if name.startswith("tu-")] + return { + "encoders": names, + "timm_backed_encoders": timm_backed, + "total": len(names), + "timm_backed_total": len(timm_backed), + } + + +service_app = FastAPI(title="Encoders Service") +service_app.include_router(router) diff --git a/MAGE/tests/test_main.py b/MAGE/tests/test_main.py index 0d00073f..73457925 100644 --- a/MAGE/tests/test_main.py +++ b/MAGE/tests/test_main.py @@ -68,6 +68,25 @@ def is_model_pretrained(self, model_id: str) -> bool: return bool(self._pretrained.get(model_id, False)) + + +class FakeSMPEncoders: + """Minimal fake of SMP encoders registry.""" + + @staticmethod + def get_encoder_names() -> list[str]: + return ["resnet34", "tu-efficientnet_b0", "tu-convnext_tiny"] + + +class FakeSMP(ModuleType): + """In-memory stand-in for ``segmentation_models_pytorch``.""" + + def __init__(self) -> None: + super().__init__("segmentation_models_pytorch") + self.__version__ = "0.0.0" + self.encoders = FakeSMPEncoders() + + class FakeMCPApp: """Minimal fake app exposing the MCP ping route.""" @@ -148,6 +167,7 @@ def app_module(monkeypatch: pytest.MonkeyPatch) -> ModuleType: """ monkeypatch.setitem(sys.modules, "timm", FakeTimm()) + monkeypatch.setitem(sys.modules, "segmentation_models_pytorch", FakeSMP()) fastmcp_mod = ModuleType("fastmcp") fastmcp_mod.FastMCP = FakeFastMCP @@ -329,3 +349,13 @@ def boom_import(name: str, *args: object, **kwargs: object) -> object: check(response.status_code == HTTP_OK, "Expected 200 on /libraries with error") ai = response.json()["AI"] check(ai["keras"] is None, "keras should be None on unexpected import error") + + +def test_encoders_list(client: TestClient) -> None: + """`/encoders` should expose SMP encoders including TIMM-backed ones.""" + response = client.get("/encoders") + check(response.status_code == HTTP_OK, "Expected 200 on /encoders") + data = response.json() + check("encoders" in data, "Missing encoders key") + check("timm_backed_encoders" in data, "Missing timm_backed_encoders key") + check("tu-efficientnet_b0" in data["timm_backed_encoders"], "Expected tu- encoder")