security: fail closed for guest and production YOLO scope

This commit is contained in:
2026-08-26 21:08:13 +02:00
parent fb1b776843
commit c0dcf573a8
+78 -283
View File
@@ -28,8 +28,10 @@ class Settings(BaseSettings):
le=604_800,
validation_alias="GEOINTEL_AUTH_SESSION_TTL_SECONDS",
)
# Guest access is an explicit demo-only capability. Production and generic
# deployments must fail closed unless an operator deliberately enables it.
guest_access_enabled: bool = Field(
default=True,
default=False,
validation_alias="GEOINTEL_GUEST_ACCESS_ENABLED",
)
guest_display_name: str = Field(
@@ -49,8 +51,6 @@ class Settings(BaseSettings):
validation_alias="DATABASE_URL",
)
storage_root: str = Field(default="./storage", validation_alias="STORAGE_ROOT")
# Analysis consumes only artifacts under storage_root. Provisioning
# workflows that stage tiles elsewhere before ingest can opt out.
allow_external_artifact_paths: bool = Field(
default=False, validation_alias="GEOINTEL_ALLOW_EXTERNAL_ARTIFACT_PATHS"
)
@@ -88,35 +88,12 @@ class Settings(BaseSettings):
default="https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl",
validation_alias="SOURCE_CATALOG_STATBEL_DCAT_URL",
)
source_catalog_statbel_max_response_mb: int = Field(
default=5,
ge=1,
le=10,
validation_alias="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB",
)
source_catalog_probe_timeout_seconds: int = Field(
default=10,
ge=1,
le=60,
validation_alias="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS",
)
source_catalog_probe_max_response_mb: int = Field(
default=2,
ge=1,
le=10,
validation_alias="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB",
)
source_catalog_probe_cache_ttl_seconds: int = Field(
default=900,
ge=0,
le=86_400,
validation_alias="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS",
)
source_catalog_statbel_max_response_mb: int = Field(default=5, ge=1, le=10, validation_alias="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB")
source_catalog_probe_timeout_seconds: int = Field(default=10, ge=1, le=60, validation_alias="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS")
source_catalog_probe_max_response_mb: int = Field(default=2, ge=1, le=10, validation_alias="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB")
source_catalog_probe_cache_ttl_seconds: int = Field(default=900, ge=0, le=86_400, validation_alias="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS")
grb_enabled: bool = Field(default=True, validation_alias="GRB_ENABLED")
grb_ogc_api_url: str = Field(
default="https://geo.api.vlaanderen.be/GRB/ogc/features/v1",
validation_alias="GRB_OGC_API_URL",
)
grb_ogc_api_url: str = Field(default="https://geo.api.vlaanderen.be/GRB/ogc/features/v1", validation_alias="GRB_OGC_API_URL")
grb_min_side_m: float = Field(default=10.0, gt=0, validation_alias="GRB_MIN_SIDE_M")
grb_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="GRB_MAX_SIDE_M")
grb_page_size: int = Field(default=1000, ge=1, le=1000, validation_alias="GRB_PAGE_SIZE")
@@ -124,99 +101,28 @@ class Settings(BaseSettings):
grb_max_features: int = Field(default=150_000, ge=1, validation_alias="GRB_MAX_FEATURES")
grb_timeout_seconds: int = Field(default=180, ge=1, le=600, validation_alias="GRB_TIMEOUT_SECONDS")
grb_max_response_mb: int = Field(default=20, ge=1, le=100, validation_alias="GRB_MAX_RESPONSE_MB")
grb_max_total_response_mb: int = Field(
default=256,
ge=1,
le=2048,
validation_alias="GRB_MAX_TOTAL_RESPONSE_MB",
)
grb_max_total_response_mb: int = Field(default=256, ge=1, le=2048, validation_alias="GRB_MAX_TOTAL_RESPONSE_MB")
grb_cache_ttl_hours: int = Field(default=24, ge=0, le=8760, validation_alias="GRB_CACHE_TTL_HOURS")
official_vector_enabled: bool = Field(default=True, validation_alias="OFFICIAL_VECTOR_ENABLED")
bwk_wfs_url: str = Field(
default="https://geo.api.vlaanderen.be/BWK/wfs",
validation_alias="BWK_WFS_URL",
)
dov_soil_wfs_url: str = Field(
default="https://www.dov.vlaanderen.be/geoserver/wfs",
validation_alias="DOV_SOIL_WFS_URL",
)
official_vector_min_side_m: float = Field(
default=10.0,
gt=0,
validation_alias="OFFICIAL_VECTOR_MIN_SIDE_M",
)
official_vector_max_side_m: float = Field(
default=20_000.0,
gt=0,
validation_alias="OFFICIAL_VECTOR_MAX_SIDE_M",
)
official_vector_page_size: int = Field(
default=1000,
ge=1,
le=2000,
validation_alias="OFFICIAL_VECTOR_PAGE_SIZE",
)
official_vector_max_pages: int = Field(
default=200,
ge=1,
le=1000,
validation_alias="OFFICIAL_VECTOR_MAX_PAGES",
)
official_vector_max_features: int = Field(
default=100_000,
ge=1,
validation_alias="OFFICIAL_VECTOR_MAX_FEATURES",
)
official_vector_timeout_seconds: int = Field(
default=180,
ge=1,
le=600,
validation_alias="OFFICIAL_VECTOR_TIMEOUT_SECONDS",
)
official_vector_max_response_mb: int = Field(
default=20,
ge=1,
le=100,
validation_alias="OFFICIAL_VECTOR_MAX_RESPONSE_MB",
)
official_vector_max_total_response_mb: int = Field(
default=256,
ge=1,
le=2048,
validation_alias="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB",
)
official_vector_cache_ttl_hours: int = Field(
default=24,
ge=0,
le=8760,
validation_alias="OFFICIAL_VECTOR_CACHE_TTL_HOURS",
)
bwk_wfs_url: str = Field(default="https://geo.api.vlaanderen.be/BWK/wfs", validation_alias="BWK_WFS_URL")
dov_soil_wfs_url: str = Field(default="https://www.dov.vlaanderen.be/geoserver/wfs", validation_alias="DOV_SOIL_WFS_URL")
official_vector_min_side_m: float = Field(default=10.0, gt=0, validation_alias="OFFICIAL_VECTOR_MIN_SIDE_M")
official_vector_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="OFFICIAL_VECTOR_MAX_SIDE_M")
official_vector_page_size: int = Field(default=1000, ge=1, le=2000, validation_alias="OFFICIAL_VECTOR_PAGE_SIZE")
official_vector_max_pages: int = Field(default=200, ge=1, le=1000, validation_alias="OFFICIAL_VECTOR_MAX_PAGES")
official_vector_max_features: int = Field(default=100_000, ge=1, validation_alias="OFFICIAL_VECTOR_MAX_FEATURES")
official_vector_timeout_seconds: int = Field(default=180, ge=1, le=600, validation_alias="OFFICIAL_VECTOR_TIMEOUT_SECONDS")
official_vector_max_response_mb: int = Field(default=20, ge=1, le=100, validation_alias="OFFICIAL_VECTOR_MAX_RESPONSE_MB")
official_vector_max_total_response_mb: int = Field(default=256, ge=1, le=2048, validation_alias="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB")
official_vector_cache_ttl_hours: int = Field(default=24, ge=0, le=8760, validation_alias="OFFICIAL_VECTOR_CACHE_TTL_HOURS")
spw_picc_enabled: bool = Field(default=True, validation_alias="SPW_PICC_ENABLED")
spw_picc_mapserver_url: str = Field(
default=(
"https://geoservices.wallonie.be/arcgis/rest/services/"
"TOPOGRAPHIE/PICC_VDIFF/MapServer"
),
validation_alias="SPW_PICC_MAPSERVER_URL",
)
spw_picc_mapserver_url: str = Field(default="https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer", validation_alias="SPW_PICC_MAPSERVER_URL")
spw_flood_hazard_enabled: bool = Field(default=True, validation_alias="SPW_FLOOD_HAZARD_ENABLED")
spw_flood_hazard_mapserver_url: str = Field(
default=(
"https://geoservices.wallonie.be/arcgis/rest/services/"
"EAU/ALEA_INOND/MapServer"
),
validation_alias="SPW_FLOOD_HAZARD_MAPSERVER_URL",
)
spw_flood_hazard_mapserver_url: str = Field(default="https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer", validation_alias="SPW_FLOOD_HAZARD_MAPSERVER_URL")
urbis_enabled: bool = Field(default=True, validation_alias="URBIS_ENABLED")
urbis_wfs_url: str = Field(
default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows",
validation_alias="URBIS_WFS_URL",
)
urbis_wfs_url: str = Field(default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows", validation_alias="URBIS_WFS_URL")
dhmv_enabled: bool = Field(default=True, validation_alias="DHMV_ENABLED")
dhmv_wcs_url: str = Field(
default="https://geo.api.vlaanderen.be/DHMV/wcs",
validation_alias="DHMV_WCS_URL",
)
dhmv_wcs_url: str = Field(default="https://geo.api.vlaanderen.be/DHMV/wcs", validation_alias="DHMV_WCS_URL")
dhmv_resolution_m: float = Field(default=5.0, ge=1.0, le=10.0, validation_alias="DHMV_RESOLUTION_M")
dhmv_min_side_m: float = Field(default=10.0, gt=0, validation_alias="DHMV_MIN_SIDE_M")
dhmv_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="DHMV_MAX_SIDE_M")
@@ -224,10 +130,7 @@ class Settings(BaseSettings):
dhmv_timeout_seconds: int = Field(default=300, ge=1, validation_alias="DHMV_TIMEOUT_SECONDS")
dhmv_max_response_mb: int = Field(default=160, ge=1, validation_alias="DHMV_MAX_RESPONSE_MB")
flood_hazard_enabled: bool = Field(default=True, validation_alias="FLOOD_HAZARD_ENABLED")
flood_hazard_wcs_url: str = Field(
default="https://geoservice.waterinfo.be/OGRK/wcs",
validation_alias="FLOOD_HAZARD_WCS_URL",
)
flood_hazard_wcs_url: str = Field(default="https://geoservice.waterinfo.be/OGRK/wcs", validation_alias="FLOOD_HAZARD_WCS_URL")
flood_hazard_resolution_m: float = Field(default=5.0, ge=2.0, le=20.0, validation_alias="FLOOD_HAZARD_RESOLUTION_M")
flood_hazard_min_side_m: float = Field(default=10.0, gt=0, validation_alias="FLOOD_HAZARD_MIN_SIDE_M")
flood_hazard_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="FLOOD_HAZARD_MAX_SIDE_M")
@@ -235,138 +138,49 @@ class Settings(BaseSettings):
flood_hazard_timeout_seconds: int = Field(default=300, ge=1, validation_alias="FLOOD_HAZARD_TIMEOUT_SECONDS")
flood_hazard_max_response_mb: int = Field(default=160, ge=1, validation_alias="FLOOD_HAZARD_MAX_RESPONSE_MB")
bathymetry_profiles_enabled: bool = Field(default=True, validation_alias="BATHYMETRY_PROFILES_ENABLED")
bathymetry_profiles_layer_url: str = Field(
default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0",
validation_alias="BATHYMETRY_PROFILES_LAYER_URL",
)
bathymetry_watercourse_layer_url: str = Field(
default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1",
validation_alias="BATHYMETRY_WATERCOURSE_LAYER_URL",
)
bathymetry_profiles_page_size: int = Field(
default=1000,
ge=1,
le=2000,
validation_alias="BATHYMETRY_PROFILES_PAGE_SIZE",
)
bathymetry_profiles_max_features: int = Field(
default=50_000,
ge=1,
le=250_000,
validation_alias="BATHYMETRY_PROFILES_MAX_FEATURES",
)
bathymetry_profiles_max_pages: int = Field(
default=200,
ge=1,
le=5_000,
validation_alias="BATHYMETRY_PROFILES_MAX_PAGES",
)
bathymetry_profiles_timeout_seconds: int = Field(
default=120,
ge=1,
le=600,
validation_alias="BATHYMETRY_PROFILES_TIMEOUT_SECONDS",
)
bathymetry_profiles_max_response_mb: int = Field(
default=32,
ge=1,
le=256,
validation_alias="BATHYMETRY_PROFILES_MAX_RESPONSE_MB",
)
bathymetry_raster_max_pixels: int = Field(
default=30_000_000,
ge=1,
validation_alias="BATHYMETRY_RASTER_MAX_PIXELS",
)
bathymetry_profiles_layer_url: str = Field(default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0", validation_alias="BATHYMETRY_PROFILES_LAYER_URL")
bathymetry_watercourse_layer_url: str = Field(default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1", validation_alias="BATHYMETRY_WATERCOURSE_LAYER_URL")
bathymetry_profiles_page_size: int = Field(default=1000, ge=1, le=2000, validation_alias="BATHYMETRY_PROFILES_PAGE_SIZE")
bathymetry_profiles_max_features: int = Field(default=50_000, ge=1, le=250_000, validation_alias="BATHYMETRY_PROFILES_MAX_FEATURES")
bathymetry_profiles_max_pages: int = Field(default=200, ge=1, le=5_000, validation_alias="BATHYMETRY_PROFILES_MAX_PAGES")
bathymetry_profiles_timeout_seconds: int = Field(default=120, ge=1, le=600, validation_alias="BATHYMETRY_PROFILES_TIMEOUT_SECONDS")
bathymetry_profiles_max_response_mb: int = Field(default=32, ge=1, le=256, validation_alias="BATHYMETRY_PROFILES_MAX_RESPONSE_MB")
bathymetry_raster_max_pixels: int = Field(default=30_000_000, ge=1, validation_alias="BATHYMETRY_RASTER_MAX_PIXELS")
mdk_bathymetry_probe_enabled: bool = Field(default=True, validation_alias="MDK_BATHYMETRY_PROBE_ENABLED")
mdk_bathymetry_wcs_url: str = Field(
default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs",
validation_alias="MDK_BATHYMETRY_WCS_URL",
)
mdk_bathymetry_probe_timeout_seconds: int = Field(
default=20,
ge=1,
le=120,
validation_alias="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS",
)
mdk_bathymetry_probe_max_response_mb: int = Field(
default=4,
ge=1,
le=16,
validation_alias="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB",
)
mdk_bathymetry_wcs_url: str = Field(default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs", validation_alias="MDK_BATHYMETRY_WCS_URL")
mdk_bathymetry_probe_timeout_seconds: int = Field(default=20, ge=1, le=120, validation_alias="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS")
mdk_bathymetry_probe_max_response_mb: int = Field(default=4, ge=1, le=16, validation_alias="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB")
thematic_raster_enabled: bool = Field(default=True, validation_alias="THEMATIC_RASTER_ENABLED")
thematic_raster_wcs_url: str = Field(
default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs",
validation_alias="THEMATIC_RASTER_WCS_URL",
)
mdk_bathymetry_acquisition_enabled: bool = Field(
default=False,
validation_alias="MDK_BATHYMETRY_ACQUISITION_ENABLED",
)
thematic_raster_wcs_url: str = Field(default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs", validation_alias="THEMATIC_RASTER_WCS_URL")
mdk_bathymetry_acquisition_enabled: bool = Field(default=False, validation_alias="MDK_BATHYMETRY_ACQUISITION_ENABLED")
mdk_bathymetry_coverage_id: str | None = Field(default=None, validation_alias="MDK_BATHYMETRY_COVERAGE_ID")
mdk_bathymetry_request_crs: str = Field(default="EPSG:4326", validation_alias="MDK_BATHYMETRY_REQUEST_CRS")
mdk_bathymetry_max_bbox_deg2: float = Field(
default=0.25,
gt=0,
validation_alias="MDK_BATHYMETRY_MAX_BBOX_DEG2",
)
mdk_bathymetry_acquisition_timeout_seconds: int = Field(
default=120,
ge=1,
validation_alias="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS",
)
mdk_bathymetry_acquisition_max_response_mb: int = Field(
default=160,
ge=1,
validation_alias="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB",
)
mdk_bathymetry_max_bbox_deg2: float = Field(default=0.25, gt=0, validation_alias="MDK_BATHYMETRY_MAX_BBOX_DEG2")
mdk_bathymetry_acquisition_timeout_seconds: int = Field(default=120, ge=1, validation_alias="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS")
mdk_bathymetry_acquisition_max_response_mb: int = Field(default=160, ge=1, validation_alias="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB")
thematic_raster_min_side_m: float = Field(default=100.0, gt=0, validation_alias="THEMATIC_RASTER_MIN_SIDE_M")
thematic_raster_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="THEMATIC_RASTER_MAX_SIDE_M")
thematic_raster_max_pixels: int = Field(default=30_000_000, ge=1, validation_alias="THEMATIC_RASTER_MAX_PIXELS")
thematic_raster_timeout_seconds: int = Field(default=300, ge=1, validation_alias="THEMATIC_RASTER_TIMEOUT_SECONDS")
thematic_raster_max_response_mb: int = Field(default=160, ge=1, validation_alias="THEMATIC_RASTER_MAX_RESPONSE_MB")
walous_enabled: bool = Field(default=True, validation_alias="WALOUS_ENABLED")
walous_source_dir: str = Field(
default="/app/storage/source-cache/walous",
validation_alias="WALOUS_SOURCE_DIR",
)
walous_analysis_resolution_m: float = Field(
default=10.0,
ge=1.0,
le=100.0,
validation_alias="WALOUS_ANALYSIS_RESOLUTION_M",
)
walous_source_dir: str = Field(default="/app/storage/source-cache/walous", validation_alias="WALOUS_SOURCE_DIR")
walous_analysis_resolution_m: float = Field(default=10.0, ge=1.0, le=100.0, validation_alias="WALOUS_ANALYSIS_RESOLUTION_M")
walous_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="WALOUS_MAX_SIDE_M")
walous_max_pixels: int = Field(default=36_000_000, ge=1, validation_alias="WALOUS_MAX_PIXELS")
spw_terrain_enabled: bool = Field(default=True, validation_alias="SPW_TERRAIN_ENABLED")
spw_terrain_source_dir: str = Field(
default="/app/storage/source-cache/spw-terrain",
validation_alias="SPW_TERRAIN_SOURCE_DIR",
)
spw_terrain_analysis_resolution_m: float = Field(
default=5.0,
ge=1.0,
le=10.0,
validation_alias="SPW_TERRAIN_ANALYSIS_RESOLUTION_M",
)
spw_terrain_source_dir: str = Field(default="/app/storage/source-cache/spw-terrain", validation_alias="SPW_TERRAIN_SOURCE_DIR")
spw_terrain_analysis_resolution_m: float = Field(default=5.0, ge=1.0, le=10.0, validation_alias="SPW_TERRAIN_ANALYSIS_RESOLUTION_M")
spw_terrain_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="SPW_TERRAIN_MAX_SIDE_M")
spw_terrain_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="SPW_TERRAIN_MAX_PIXELS")
redis_url: str | None = Field(default=None, validation_alias="REDIS_URL")
log_level: str = Field(default="INFO", validation_alias="GEOINTEL_LOG_LEVEL")
sql_log_level: str = Field(default="WARNING", validation_alias="GEOINTEL_SQL_LOG_LEVEL")
reconcile_interrupted_runs_on_startup: bool = Field(
default=False,
validation_alias="GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP",
)
reconcile_interrupted_runs_on_startup: bool = Field(default=False, validation_alias="GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP")
aoi_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AOI_WORKER_ENABLED")
aoi_worker_poll_seconds: float = Field(default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_AOI_WORKER_POLL_SECONDS")
# Executes queued detection.run / segmentation.run jobs so tiled GPU
# inference never blocks an HTTP request.
analysis_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_ANALYSIS_WORKER_ENABLED")
analysis_worker_poll_seconds: float = Field(
default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS"
)
analysis_worker_poll_seconds: float = Field(default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS")
database_statement_timeout_ms: int = Field(default=5_000, validation_alias="DATABASE_STATEMENT_TIMEOUT_MS")
yolo_enabled: bool = Field(default=False, validation_alias="YOLO_ENABLED")
yolo_models_dir: str = Field(default="/app/models", validation_alias="YOLO_MODELS_DIR")
@@ -375,18 +189,12 @@ class Settings(BaseSettings):
yolo_model_display_name: str = Field(default="Configured YOLO detector", validation_alias="YOLO_MODEL_DISPLAY_NAME")
yolo_model_version: str | None = Field(default=None, validation_alias="YOLO_MODEL_VERSION")
yolo_model_classes: str = Field(default="building", validation_alias="YOLO_MODEL_CLASSES")
yolo_enforce_validation_scope: bool = Field(default=False, validation_alias="YOLO_ENFORCE_VALIDATION_SCOPE")
yolo_validation_scope_manifest_path: str | None = Field(
default=None,
validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_PATH",
)
yolo_validation_scope_manifest_sha256: str | None = Field(
default=None,
validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256",
)
# Deprecated compatibility field. Mutable Area names are never an
# inference authorization boundary; deployments must use the immutable
# checksum-bound scope manifest above.
# Serving configured YOLO is validation-scoped by default. Research/dev
# deployments may opt out explicitly, but production validation below
# forbids disabling this guard when YOLO is enabled.
yolo_enforce_validation_scope: bool = Field(default=True, validation_alias="YOLO_ENFORCE_VALIDATION_SCOPE")
yolo_validation_scope_manifest_path: str | None = Field(default=None, validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_PATH")
yolo_validation_scope_manifest_sha256: str | None = Field(default=None, validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256")
yolo_validated_area_names: str = Field(default="Mol,Kempen", validation_alias="YOLO_VALIDATED_AREA_NAMES")
yolo_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE")
yolo_require_cuda: bool = Field(default=False, validation_alias="YOLO_REQUIRE_CUDA")
@@ -394,57 +202,29 @@ class Settings(BaseSettings):
yolo_max_tiles: int = Field(default=100, validation_alias="YOLO_MAX_TILES")
yolo_max_detections: int = Field(default=1000, validation_alias="YOLO_MAX_DETECTIONS")
yolo_duplicate_iou_threshold: float = Field(default=0.5, ge=0.0, le=1.0, validation_alias="YOLO_DUPLICATE_IOU_THRESHOLD")
yolo_suppress_tile_edge_detections: bool = Field(
default=True, validation_alias="YOLO_SUPPRESS_TILE_EDGE_DETECTIONS"
)
# Intersection over the smaller box. The candidate evaluation freezes this
# during calibration; serving a promoted model at a different value means
# the runtime suppresses detections the gate counted.
yolo_containment_nms_threshold: float = Field(
default=0.85, ge=0.0, le=1.0, validation_alias="YOLO_CONTAINMENT_NMS_THRESHOLD"
)
yolo_suppress_tile_edge_detections: bool = Field(default=True, validation_alias="YOLO_SUPPRESS_TILE_EDGE_DETECTIONS")
yolo_containment_nms_threshold: float = Field(default=0.85, ge=0.0, le=1.0, validation_alias="YOLO_CONTAINMENT_NMS_THRESHOLD")
yolo_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE")
yolo_seg_enabled: bool = Field(default=False, validation_alias="YOLO_SEG_ENABLED")
yolo_seg_model_path: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_PATH")
yolo_seg_model_id: str = Field(default="yolo-seg-configured", validation_alias="YOLO_SEG_MODEL_ID")
yolo_seg_model_display_name: str = Field(
default="Configured YOLO segmentation",
validation_alias="YOLO_SEG_MODEL_DISPLAY_NAME",
)
yolo_seg_model_display_name: str = Field(default="Configured YOLO segmentation", validation_alias="YOLO_SEG_MODEL_DISPLAY_NAME")
yolo_seg_model_version: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_VERSION")
sam_enabled: bool = Field(default=False, validation_alias="SAM_ENABLED")
sam_model_path: str | None = Field(default=None, validation_alias="SAM_MODEL_PATH")
sam_model_id: str = Field(default="sam-configured", validation_alias="SAM_MODEL_ID")
sam_model_display_name: str = Field(
default="Configured SAM segmentation",
validation_alias="SAM_MODEL_DISPLAY_NAME",
)
sam_model_display_name: str = Field(default="Configured SAM segmentation", validation_alias="SAM_MODEL_DISPLAY_NAME")
sam_model_version: str | None = Field(default=None, validation_alias="SAM_MODEL_VERSION")
segmentation_max_masks_per_tile: int = Field(default=300, ge=1, validation_alias="SEGMENTATION_MAX_MASKS_PER_TILE")
# Masks and boxes overlap differently, so segmentation carries its own
# containment value rather than borrowing the detector's.
segmentation_containment_nms_threshold: float = Field(
default=0.85,
ge=0.0,
le=1.0,
validation_alias="SEGMENTATION_CONTAINMENT_NMS_THRESHOLD",
)
segmentation_duplicate_iou_threshold: float = Field(
default=0.5,
ge=0.0,
le=1.0,
validation_alias="SEGMENTATION_DUPLICATE_IOU_THRESHOLD",
)
segmentation_containment_nms_threshold: float = Field(default=0.85, ge=0.0, le=1.0, validation_alias="SEGMENTATION_CONTAINMENT_NMS_THRESHOLD")
segmentation_duplicate_iou_threshold: float = Field(default=0.5, ge=0.0, le=1.0, validation_alias="SEGMENTATION_DUPLICATE_IOU_THRESHOLD")
ollama_enabled: bool = Field(default=False, validation_alias="OLLAMA_ENABLED")
ollama_base_url: str = Field(default="http://127.0.0.1:11434", validation_alias="OLLAMA_BASE_URL")
ollama_default_model: str = Field(default="qwen3.5:9b", validation_alias="OLLAMA_DEFAULT_MODEL")
ollama_timeout_seconds: int = Field(default=120, ge=5, le=600, validation_alias="OLLAMA_TIMEOUT_SECONDS")
ollama_max_output_tokens: int = Field(default=1_200, ge=100, le=4_000, validation_alias="OLLAMA_MAX_OUTPUT_TOKENS")
ollama_context_tokens: int = Field(default=16_384, ge=4_096, le=131_072, validation_alias="OLLAMA_CONTEXT_TOKENS")
cors_origins: list[str] | str = Field(
default=["http://localhost:5173", "http://127.0.0.1:5173"],
validation_alias="CORS_ORIGINS",
)
cors_origins: list[str] | str = Field(default=["http://localhost:5173", "http://127.0.0.1:5173"], validation_alias="CORS_ORIGINS")
@field_validator("cors_origins", mode="before")
@classmethod
@@ -466,18 +246,33 @@ class Settings(BaseSettings):
return normalized
@model_validator(mode="after")
def validate_operator_auth(self) -> "Settings":
def validate_operator_auth_and_production_ai(self) -> "Settings":
self.guest_display_name = self.guest_display_name.strip()
if not self.guest_display_name:
raise ValueError("GEOINTEL_GUEST_DISPLAY_NAME must not be blank")
if not self.auth_enabled:
return self
if self.auth_enabled:
if not (self.auth_username or "").strip():
raise ValueError("GEOINTEL_AUTH_USERNAME is required when authentication is enabled")
if not (self.auth_password_hash or "").startswith("pbkdf2_sha256$"):
raise ValueError("GEOINTEL_AUTH_PASSWORD_HASH must be a PBKDF2-SHA256 hash")
if len(self.auth_session_secret or "") < 32:
raise ValueError("GEOINTEL_AUTH_SESSION_SECRET must contain at least 32 characters")
production = self.app_env.strip().lower() in {"prod", "production"}
if production and self.yolo_enabled:
if not self.yolo_enforce_validation_scope:
raise ValueError(
"Production YOLO requires GEOINTEL_YOLO_ENFORCE_VALIDATION_SCOPE=true"
)
if not (self.yolo_validation_scope_manifest_path or "").strip():
raise ValueError(
"Production YOLO requires GEOINTEL_YOLO_VALIDATION_SCOPE_MANIFEST_PATH"
)
manifest_hash = (self.yolo_validation_scope_manifest_sha256 or "").strip().lower()
if len(manifest_hash) != 64 or any(ch not in "0123456789abcdef" for ch in manifest_hash):
raise ValueError(
"Production YOLO requires a 64-character SHA-256 validation scope hash"
)
return self