-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
2278 lines (1978 loc) · 87.9 KB
/
Copy pathapp.py
File metadata and controls
2278 lines (1978 loc) · 87.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ruff: noqa: E402
import json
import logging
import os
import re
import secrets
import sys
import time
from datetime import UTC, datetime, timedelta
from pathlib import Path
from threading import Lock, Thread
from urllib.parse import urlparse
from dotenv import load_dotenv
from flask import (
Blueprint,
Flask,
Response,
current_app,
flash,
g,
has_request_context,
jsonify,
redirect,
render_template,
request,
)
from flask_login import current_user
from werkzeug.exceptions import HTTPException
from werkzeug.local import LocalProxy
from werkzeug.middleware.proxy_fix import ProxyFix
from backend.auth.api_decorators import api_login_required, api_session_login_required
from backend.bootstrap_compat import apply_runtime_compatibility_patches
from backend.llm_gateway.latency_metrics import ai_latency_metrics_prometheus_lines
from backend.logging_config import configure_structured_logging
from backend.mcp_server.connector_metrics import connector_metrics_prometheus_lines
from backend.observability.crash_reporting import (
capture_exception_with_fallback,
crash_reporting_prometheus_lines,
initialize_crash_reporting,
)
from backend.observability.latency_slo import latency_slo_prometheus_lines
from backend.product_version import PRODUCT_VERSION
from backend.runtime import (
APP_SERVICE_KEYS,
ApplicationRuntime,
LifecycleResult,
PodmanDataPlaneManager,
RuntimePhase,
ServiceState,
get_application_runtime,
)
from backend.runtime.application import default_runtime_root
from backend.storage.retained_data import discover_retained_data
from backend.security.secret_resolver import (
is_secure_secret_source,
resolve_runtime_secret,
)
from extensions import limiter
logger = logging.getLogger(__name__)
core_bp = Blueprint("dle_core", __name__)
# Security: Warn about default credentials in production
def validate_production_security(app: Flask) -> None:
"""Validate that no default/insecure credentials are in use in production."""
is_production = bool(app.config.get("DLE_PRODUCTION_MODE"))
# Check for default admin credentials
admin_user = os.environ.get("ADMIN_USERNAME", "")
admin_pass = os.environ.get("ADMIN_PASSWORD", "")
insecure_usernames = {"admin", "administrator", "root", "test", "user"}
insecure_passwords = {"admin", "admin123", "password", "password123", "123456", "test", "root"}
if is_production:
issues = []
if admin_user and admin_user.lower() in insecure_usernames:
issues.append("Default admin username detected")
if admin_pass and (admin_pass in insecure_passwords or len(admin_pass) < 12):
issues.append("Insecure admin password (use min 12 chars)")
if not app.secret_key:
issues.append("SESSION_SECRET not set")
elif not is_secure_secret_source(app.config.get("DLE_SESSION_SECRET_SOURCE", "missing")):
issues.append(
"SESSION_SECRET is not vault-backed "
f"(source={app.config.get('DLE_SESSION_SECRET_SOURCE', 'missing')})"
)
if issues:
logger.error(f"SECURITY: Production security issues: {', '.join(issues)}")
logger.error("SECURITY: Please fix these issues before deploying to production!")
else:
# Development warnings only
if admin_user and admin_user.lower() in insecure_usernames:
logger.warning("SECURITY WARNING: Using default admin username. Change before deployment!")
if admin_pass and admin_pass in insecure_passwords:
logger.warning("SECURITY WARNING: Using default admin password. Change before deployment!")
# Server configuration remains a constant default; each entry point resolves its
# final port from the application configuration.
DEFAULT_PORT = int(os.environ.get("PORT", 5000))
# Maps retired/legacy prefixes to canonical /api/v1 successors (deprecation headers).
# Regulatory content that used to be dual-mounted under /api/compliance now lives only
# under /api/v1/regulatory (compliance_bp owns /api/v1/compliance for axis-7 standards).
LEGACY_API_PREFIXES = {
"/api/compliance": "/api/v1/regulatory",
"/api/regulatory": "/api/v1/regulatory",
"/api/ka": "/api/v1/ka",
"/api/mcp": "/api/v1/mcp",
"/api/persona": "/api/v1/persona",
"/api/pillar": "/api/v1/pillar",
"/api/simulations": "/api/v1/simulations",
"/api/truth": "/api/v1/truth",
"/api/ukg": "/api/v1",
"/api/v1/ukg": "/api/v1",
}
LEGACY_API_SUNSET = "Wed, 30 Sep 2026 00:00:00 GMT"
def _metric_route_label() -> str:
"""Return a low-cardinality route label for request metrics."""
if request.url_rule and request.url_rule.rule:
return request.url_rule.rule
return "unmatched"
def _prometheus_label_value(value: str) -> str:
"""Escape Prometheus label values safely."""
return str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
def _current_correlation_id() -> str | None:
return getattr(g, "correlation_id", None) if has_request_context() else None
@core_bp.before_app_request
def track_request_metrics_start():
"""Track aggregate request counts for lightweight operational metrics."""
g.request_started_at = time.perf_counter()
get_application_runtime().metrics.begin_request()
@core_bp.before_app_request
def enforce_runtime_admission():
"""Reject new mutations while startup, shutdown, or lifecycle work drains."""
runtime = get_application_runtime()
if runtime.admits_request(request.method, request.path):
return None
return jsonify(
{
"success": False,
"error": "Application runtime is not accepting new work",
"code": "RUNTIME_NOT_ACCEPTING_WORK",
"phase": runtime.phase.value,
}
), 503
@core_bp.after_app_request
def track_request_metrics_end(response):
"""Ensure in-flight counter is decremented for all completed responses."""
route_label = _metric_route_label()
method = request.method.upper()
duration_ms = max(
0.0,
(time.perf_counter() - getattr(g, "request_started_at", time.perf_counter())) * 1000.0,
)
get_application_runtime().metrics.record_request(
method,
route_label,
response.status_code,
duration_ms,
)
return response
def _sanitize_server_error_payload(payload: dict) -> tuple[dict, bool]:
fallback = "An internal error occurred. Please try again later."
changed = False
if isinstance(payload.get("error"), str):
original = payload["error"]
sanitized = normalize_public_error_message(original, fallback)
if sanitized != original:
payload["error"] = sanitized
changed = True
error_obj = payload.get("error")
if isinstance(error_obj, dict):
message = error_obj.get("message")
if isinstance(message, str):
sanitized = normalize_public_error_message(message, fallback)
if sanitized != message:
error_obj["message"] = sanitized
changed = True
if isinstance(payload.get("message"), str):
original = payload["message"]
sanitized = normalize_public_error_message(original, fallback)
if sanitized != original:
payload["message"] = sanitized
changed = True
return payload, changed
def _legacy_api_successor_path(path: str) -> str | None:
for legacy_prefix, canonical_prefix in LEGACY_API_PREFIXES.items():
if path == legacy_prefix or path.startswith(f"{legacy_prefix}/"):
return f"{canonical_prefix}{path[len(legacy_prefix):]}"
return None
def _split_config_values(raw_value) -> list[str]:
if not raw_value:
return []
if isinstance(raw_value, str):
return [item.strip() for item in raw_value.split(",") if item.strip()]
if isinstance(raw_value, (list, tuple, set)):
return [str(item).strip() for item in raw_value if str(item).strip()]
return []
def _hostname_from_value(raw_value: str) -> str:
value = (raw_value or "").strip().lower()
if not value:
return ""
if "://" in value:
return (urlparse(value).hostname or "").lower().rstrip(".")
return (urlparse(f"//{value}").hostname or value.split(":", 1)[0]).lower().rstrip(".")
def _trusted_hosts() -> set[str]:
raw_hosts = current_app.config.get("TRUSTED_HOSTS") or os.environ.get("TRUSTED_HOSTS")
hosts = {_hostname_from_value(host) for host in _split_config_values(raw_hosts)}
server_name = current_app.config.get("SERVER_NAME")
if server_name:
hosts.add(_hostname_from_value(server_name))
canonical_origin = current_app.config.get("CANONICAL_EXTERNAL_ORIGIN") or os.environ.get("CANONICAL_EXTERNAL_ORIGIN")
if canonical_origin:
hosts.add(_hostname_from_value(canonical_origin))
return {host for host in hosts if host}
def _request_hostname() -> str:
raw_host = request.environ.get("HTTP_HOST") or request.environ.get("SERVER_NAME") or ""
return _hostname_from_value(raw_host)
@core_bp.before_app_request
def validate_trusted_host():
"""Reject untrusted Host values when a host policy is configured."""
if current_app.config.get("DLE_DESKTOP_MODE"):
if _request_hostname() not in {"localhost", "127.0.0.1", "::1"}:
return jsonify(
{
"error": "Untrusted desktop host",
"success": False,
"code": "UNTRUSTED_DESKTOP_HOST",
}
), 400
return None
trusted_hosts = _trusted_hosts()
if not trusted_hosts:
if current_app.config.get("DLE_PRODUCTION_MODE") and not current_app.config.get("TESTING"):
return jsonify(
{
"error": "Trusted host policy is not configured",
"success": False,
"code": "TRUSTED_HOSTS_NOT_CONFIGURED",
}
), 500
return None
if _request_hostname() not in trusted_hosts:
return jsonify(
{
"error": "Untrusted host",
"success": False,
"code": "UNTRUSTED_HOST",
}
), 400
return None
@core_bp.before_app_request
def validate_desktop_origin():
"""Reject browser origins that cannot belong to the packaged/local renderer."""
if not current_app.config.get("DLE_DESKTOP_MODE"):
return None
origin = (request.headers.get("Origin") or "").strip().rstrip("/").lower()
if not origin:
return None
allowed_origins = {
"app://-",
"app://dashboard",
"http://localhost:3000",
"http://127.0.0.1:3000",
}
if origin not in allowed_origins:
return jsonify(
{
"error": "Untrusted desktop origin",
"success": False,
"code": "UNTRUSTED_DESKTOP_ORIGIN",
}
), 403
return None
def _redirect_target_url() -> str:
canonical_origin = current_app.config.get("CANONICAL_EXTERNAL_ORIGIN") or os.environ.get("CANONICAL_EXTERNAL_ORIGIN")
path = request.full_path if request.query_string else request.path
if canonical_origin:
return f"{canonical_origin.rstrip('/')}{path}"
return request.url.replace("http://", "https://", 1)
@core_bp.after_app_request
def normalize_server_error_payload(response):
"""Sanitize 5xx JSON payloads to block raw exception/provider leaks."""
if response.status_code < 500 or not response.is_json:
return response
payload = response.get_json(silent=True)
if not isinstance(payload, dict):
return response
sanitized_payload, changed = _sanitize_server_error_payload(payload)
if not changed:
return response
response.set_data(json.dumps(sanitized_payload))
response.headers["Content-Type"] = "application/json"
return response
@core_bp.after_app_request
def add_legacy_api_deprecation_headers(response):
successor_path = _legacy_api_successor_path(request.path)
if successor_path is None:
return response
response.headers.setdefault("Deprecation", "true")
response.headers.setdefault("Sunset", LEGACY_API_SUNSET)
response.headers.setdefault("X-DataLogicEngine-Route-Status", "legacy")
response.headers.add("Link", f'<{successor_path}>; rel="successor-version"')
return response
# Strict TLS Redirection in Production
@core_bp.before_app_request
def force_https():
"""Force HTTPS redirection in production environments."""
is_prod = current_app.config.get("DLE_PRODUCTION_MODE")
if current_app.config.get("DLE_DESKTOP_MODE"):
return None
if is_prod and not current_app.config.get("TESTING") and not request.is_secure:
return redirect(_redirect_target_url(), code=301)
# SSO Configuration
from backend.auth.sso import configure_sso
# Initialize WebSockets
from backend.websocket import init_socketio
from extensions import cache, compress, cors, csrf, db, login_manager, migrate
from models import User
def _normalize_origin(raw_origin: str) -> str:
origin = (raw_origin or "").strip()
if not origin:
return ""
parsed = urlparse(origin)
if not parsed.scheme or not parsed.netloc:
return ""
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"
def _parse_cors_origins(raw_origins):
if raw_origins is None:
return []
if isinstance(raw_origins, str):
return [item.strip() for item in raw_origins.split(",") if item.strip()]
if isinstance(raw_origins, (list, tuple, set)):
return [str(item).strip() for item in raw_origins if str(item).strip()]
return []
# Initialize Celery
# Exempt JSON API endpoints from CSRF (they use session auth or API keys)
# CSRF is still enforced on all HTML form submissions
from flask_wtf.csrf import CSRFError
from backend.auth import api_decorators as auth_api_decorators
from backend.celery_app import make_celery
from backend.security.api_csrf import is_api_csrf_enforced, validate_api_csrf_request
from backend.utils.error_normalization import normalize_public_error_message
# Single-mode / OS-level auth (auth deprecation Phase C, 2026-06-13): only the
# desktop Windows-identity endpoints remain. The former web-app auth routes
# (login/register/mfa-verify/sso-callback) were removed; their stale CSRF-exempt
# entries are dropped here.
CSRF_API_EXEMPT_PATH_PREFIXES = (
"/api/v1/auth/desktop/challenge",
"/api/v1/auth/desktop/auto-login",
)
def _request_uses_session_cookie() -> bool:
return any(
request.cookies.get(cookie_name)
for cookie_name in ("session", "session_id", "remember_token")
)
def _request_uses_stateless_auth() -> bool:
auth_header = request.headers.get("Authorization", "")
return bool(request.headers.get("X-API-Key") or auth_header.lower().startswith("bearer "))
def _request_uses_signed_desktop_auth() -> bool:
desktop_auth, _ = auth_api_decorators.check_desktop_request_auth()
return desktop_auth
def _is_trusted_origin_request() -> bool:
trusted_origins = current_app.config.get("DLE_TRUSTED_CSRF_ORIGINS", set())
origin_header = request.headers.get("Origin", "")
if origin_header:
return _normalize_origin(origin_header) in trusted_origins
referer_header = request.headers.get("Referer", "")
if referer_header:
return _normalize_origin(referer_header) in trusted_origins
return False
@core_bp.before_app_request
def csrf_for_forms_only():
if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
return None
if current_app.config.get("TESTING") or os.environ.get("FLASK_ENV") == "testing":
return None
is_api_like_request = request.path.startswith("/api/") or request.path.startswith("/graphql")
if is_api_like_request:
if _request_uses_session_cookie() and not _request_uses_stateless_auth():
if _request_uses_signed_desktop_auth():
return None
if not _is_trusted_origin_request():
return jsonify(
{
"error": "Cross-site state-changing request blocked",
"success": False,
"code": "CSRF_ORIGIN_CHECK_FAILED",
}
), 403
is_exempt = any(request.path.startswith(prefix) for prefix in CSRF_API_EXEMPT_PATH_PREFIXES)
if is_api_csrf_enforced() and not is_exempt:
csrf_ok, csrf_error = validate_api_csrf_request()
if not csrf_ok:
return jsonify(
{
"error": csrf_error or "CSRF request token invalid",
"success": False,
"code": "CSRF_TOKEN_CHECK_FAILED",
}
), 403
return None
csrf.protect()
return None
# Handle CSRF errors
@core_bp.app_errorhandler(CSRFError)
def handle_csrf_error(e):
from flask import request
if request.is_json or request.headers.get('Content-Type', '').startswith('application/json'):
return jsonify({'error': 'CSRF token missing or invalid', 'success': False}), 400
flash('Security token expired. Please try again.', 'danger')
return redirect(request.path or "/")
# Initialize Unified Middleware Stack (Hardened)
# Import models (after extensions initialization)
# Note: Importing models ensures SQLAlchemy creates their tables during db.create_all()
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from backend.middleware import setup_middleware
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
@login_manager.request_loader
def load_desktop_user_from_request(_request):
"""Authenticate signed Electron loopback requests without relying on cookies."""
from backend.auth.api_decorators import check_desktop_request_auth
is_auth, user = check_desktop_request_auth()
return user if is_auth else None
def password_meets_policy(password: str) -> bool:
"""Enforce a basic password policy for initial hardening."""
if not password or len(password) < 12:
return False
has_upper = re.search(r"[A-Z]", password)
has_lower = re.search(r"[a-z]", password)
has_digit = re.search(r"\d", password)
has_symbol = re.search(r"[^A-Za-z0-9]", password)
return all([has_upper, has_lower, has_digit, has_symbol])
def _should_auto_create_schema(app: Flask | None = None) -> bool:
"""Require explicit opt-in before mutating schema at process startup."""
if app is None:
return _env_bool("AUTO_CREATE_SCHEMA")
return bool(app.config.get("DLE_INITIALIZE_SCHEMA"))
def _initialize_database_schema(app: Flask) -> None:
"""Initialize schema only when explicitly requested for disposable environments."""
if not _should_auto_create_schema(app):
logger.info("Startup schema auto-creation disabled; use 'flask db upgrade' or backend/init_db.py.")
return
if app.config.get("DLE_PRODUCTION_MODE"):
raise RuntimeError(
"AUTO_CREATE_SCHEMA=true is not allowed in production. "
"Apply migrations explicitly with 'flask db upgrade' before startup."
)
with app.app_context():
db.create_all()
if app.config.get("DLE_DESKTOP_MODE") and app.config["SQLALCHEMY_DATABASE_URI"].startswith("sqlite"):
from backend.desktop.schema_upgrade import apply_desktop_sqlite_upgrades
upgraded_columns = apply_desktop_sqlite_upgrades(db.engine)
if upgraded_columns:
logger.info(
"Desktop SQLite schema upgraded with columns: %s",
", ".join(upgraded_columns),
)
logger.warning(
"Database tables auto-created because AUTO_CREATE_SCHEMA=true. "
"Do not enable this in managed or production environments."
)
# Multi-tenant Postgres RLS removed (single-mode / single-tenant deployment) — see
# docs/audits/DataLogicEngine_Auth_Deprecation_Plan.md (Phase D).
# MCP Routes moved to routes/mcp_routes.py (registered via routes package)
# AI Chat legacy blueprint removed (Superseded by LLM Gateway)
# KA Routes moved to routes/ka_routes.py (registered via routes package)
def _env_flag(name: str, default: bool = False) -> bool:
from backend.runtime.startup_contract import env_flag
return env_flag(name, default)
def _legacy_api_prefixes_enabled() -> bool:
"""Legacy /api/* mirrors are off by default (G-API hard-off)."""
from backend.runtime.startup_contract import legacy_api_prefixes_enabled
return legacy_api_prefixes_enabled()
def _register_application_routes(app: Flask) -> None:
"""Register canonical application blueprints in one startup location."""
legacy = _legacy_api_prefixes_enabled()
app.config["DLE_LEGACY_API_PREFIXES"] = legacy
if legacy:
logger.warning(
"DLE_LEGACY_API_PREFIXES enabled: registering legacy /api/* mirrors "
"alongside /api/v1/*"
)
from backend.truth_engine.api import truth_api
app.register_blueprint(truth_api, url_prefix='/api/v1/truth')
if legacy:
app.register_blueprint(truth_api, name='truth_legacy', url_prefix='/api/truth')
logger.info(
"Truth Engine API blueprint registered (v1%s)",
" + legacy" if legacy else "",
)
try:
from backend.persona_api import persona_api
app.register_blueprint(persona_api, url_prefix='/api/v1/persona')
if legacy:
app.register_blueprint(persona_api, name='persona_legacy', url_prefix='/api/persona')
logger.info(
"Persona API blueprint registered (v1%s)",
" + legacy" if legacy else "",
)
except ImportError as e:
logger.warning(f"Could not register Persona API blueprint: {e}")
try:
from backend.pillar_api import pillar_api
app.register_blueprint(pillar_api, url_prefix='/api/v1/pillar')
if legacy:
app.register_blueprint(pillar_api, name='pillar_legacy', url_prefix='/api/pillar')
logger.info(
"Pillar API blueprint registered (v1%s)",
" + legacy" if legacy else "",
)
except ImportError as e:
logger.warning(f"Could not register Pillar API blueprint: {e}")
try:
# P2-01: regulatory owns /api/v1/regulatory only.
# Axis-7 UKG compliance standards live on compliance_bp at /api/v1/compliance
# (registered via backend.routes.register_routes). Do not also mount
# regulatory_api on /api/v1/compliance — that shadowed /standards.
from backend.regulatory_api import regulatory_api
app.register_blueprint(regulatory_api, url_prefix='/api/v1/regulatory')
if legacy:
app.register_blueprint(
regulatory_api, name='compliance_legacy', url_prefix='/api/compliance'
)
app.register_blueprint(
regulatory_api, name='regulatory_legacy', url_prefix='/api/regulatory'
)
logger.info(
"Regulatory API blueprint registered at /api/v1/regulatory%s",
" (+ legacy /api/compliance,/api/regulatory)" if legacy else "",
)
except ImportError as e:
logger.warning(f"Could not register Regulatory API blueprint: {e}")
from backend.ukg_api import ukg_api
app.register_blueprint(ukg_api, url_prefix='/api/v1')
if legacy:
app.register_blueprint(ukg_api, name='ukg_legacy', url_prefix='/api/ukg')
app.register_blueprint(ukg_api, name='ukg_v1_legacy', url_prefix='/api/v1/ukg')
# Always keep /api/v1/ukg as an explicit alias only when legacy v1 nested path is needed;
# without legacy flag, ukg routes under /api/v1 prefix alone remain authoritative.
# Replit web auth is not part of the desktop product. Opt-in only.
if _env_flag("REPLIT_AUTH_ENABLED", default=False):
try:
from replit_auth import make_replit_blueprint
replit_bp = make_replit_blueprint()
if replit_bp:
app.register_blueprint(replit_bp, url_prefix="/auth")
logger.info("Replit Auth blueprint registered")
else:
logger.info("Replit Auth disabled (REPL_ID not set)")
except ImportError as e:
logger.warning(f"Could not register Replit Auth blueprint: {e}")
# Swagger UI only when a real swagger/openapi asset is present (avoid dead /api/docs).
swagger_candidates = [
Path(app.root_path) / "static" / "swagger.json",
Path(app.root_path) / "docs" / "openapi.yaml",
]
swagger_url = None
if (Path(app.root_path) / "static" / "swagger.json").is_file():
swagger_url = "/static/swagger.json"
if swagger_url:
from flask_swagger_ui import get_swaggerui_blueprint
swaggerui_blueprint = get_swaggerui_blueprint(
'/api/docs',
swagger_url,
config={'app_name': "DataLogicEngine API"},
)
app.register_blueprint(swaggerui_blueprint, url_prefix='/api/docs')
logger.info("Swagger UI registered at /api/docs (%s)", swagger_url)
else:
logger.info(
"Swagger UI not registered (no static/swagger.json). "
"Public contract: docs/openapi.yaml"
)
try:
from backend.tracing.api import trace_bp
app.register_blueprint(trace_bp)
logger.info("Trace API blueprint registered at /api/v1/trace")
except ImportError as e:
logger.warning(f"Could not register Trace API blueprint: {e}")
try:
from backend.llm_gateway.api import register_gateway_routes
register_gateway_routes(app)
logger.info(
"LLM Gateway API registered at /api/v1/gateway and /api/v1/admin/gateway"
)
except ImportError as e:
logger.warning(f"Could not register LLM Gateway API: {e}")
try:
from backend.routes.analytics_routes import analytics_bp
app.register_blueprint(analytics_bp)
logger.info("Analytics API registered at /api/v1/analytics")
except ImportError as e:
logger.warning(f"Could not register Analytics API: {e}")
try:
logger.debug("Registering GraphQL...")
from backend.graphql_schema import register_graphql
register_graphql(app)
# GraphQL is POST JSON only + auth required; no GraphiQL IDE surface.
logger.info("GraphQL API registered at /graphql (auth required, no GraphiQL)")
except ImportError as e:
logger.warning(f"Could not register GraphQL API: {e}")
except Exception as e:
logger.error(f"GraphQL registration error: {e}")
try:
from backend.routes.gdpr_routes import gdpr_bp
app.register_blueprint(gdpr_bp)
logger.info("GDPR API registered at /api/v1/gdpr")
except ImportError as e:
logger.warning(f"Could not register GDPR API: {e}")
try:
from backend.routes.retention_routes import retention_bp
app.register_blueprint(retention_bp)
logger.info("Retention API registered at /api/v1/retention")
except ImportError as e:
logger.warning(f"Could not register Retention API: {e}")
try:
from backend.routes.privacy_routes import privacy_bp
app.register_blueprint(privacy_bp)
logger.info("Privacy API registered at /api/v1/privacy")
except ImportError as e:
logger.warning(f"Could not register Privacy API: {e}")
from backend.routes import register_routes
register_routes(app)
def _initialize_storage_collections(app: Flask) -> dict[str, bool]:
"""Ensure ChromaDB named collections and object-storage buckets exist at startup."""
result = {"chroma": False, "object_store": False}
with app.app_context():
try:
from backend.storage.vector_store import initialize_collections
initialize_collections()
result["chroma"] = True
logger.info("ChromaDB collections initialized")
_maybe_start_db_c_indexing(app)
except Exception as exc: # pylint: disable=broad-except
logger.warning("ChromaDB collection init skipped: %s", exc)
try:
from backend.storage.object_store import get_object_store
store = get_object_store()
for bucket in [
"audit-logs",
"simulation-artifacts",
"deliverables",
"graphs",
"evaluation-data",
"trace-exports",
]:
if not store.create_bucket(bucket):
raise RuntimeError(f"required_object_bucket_unavailable:{bucket}")
result["object_store"] = True
logger.info("Object storage buckets initialized")
except Exception as exc: # pylint: disable=broad-except
logger.warning("Object storage bucket init skipped: %s", exc)
if (
app.config.get("DLE_PRODUCTION_MODE")
or app.config.get("DLE_DATA_PLANE_DRIVER") == "podman"
) and not all(result.values()):
raise RuntimeError("required_storage_initialization_failed")
return result
def _chroma_collection_counts() -> dict:
"""Return ChromaDB collection counts for health and desktop IPC."""
try:
store = current_app.extensions.get("dle_vector_store")
if store is None:
return {}
stats = store.list_collection_stats()
return {
name: int((data or {}).get("count", (data or {}).get("total_count", 0)) or 0)
for name, data in stats.items()
}
except Exception as exc: # pylint: disable=broad-except
logger.debug("ChromaDB collection counts unavailable: %s", exc)
return {}
def _redis_ping_ms() -> float | None:
"""Return Redis ping latency in milliseconds when Redis is reachable."""
try:
import redis
redis_url = current_app.config.get("DLE_REDIS_URL") or os.environ.get(
"REDIS_URL", "redis://127.0.0.1:6379/0"
)
client = redis.Redis.from_url(redis_url, socket_connect_timeout=0.5, socket_timeout=0.5)
start = time.perf_counter()
client.ping()
return round((time.perf_counter() - start) * 1000, 3)
except Exception as exc: # pylint: disable=broad-except
logger.debug("Redis ping unavailable: %s", exc)
return None
def _object_store_bucket_stats() -> dict:
"""Return object-store bucket counts and byte totals for health and desktop IPC."""
buckets = [
"audit-logs",
"simulation-artifacts",
"deliverables",
"graphs",
"evaluation-data",
"trace-exports",
]
stats: dict[str, dict[str, int | str]] = {}
try:
store = current_app.extensions.get("dle_object_store")
if store is None:
raise RuntimeError("object_store_not_initialized")
for bucket in buckets:
objects = store.list(bucket)
stats[bucket] = {
"object_count": len(objects),
"total_bytes": sum(int(getattr(obj, "size", 0) or 0) for obj in objects),
}
return {"status": "ok", "buckets": stats}
except Exception as exc: # pylint: disable=broad-except
logger.debug("Object-store bucket stats unavailable: %s", exc)
return {
"status": "unavailable",
"buckets": {
bucket: {"object_count": 0, "total_bytes": 0}
for bucket in buckets
},
}
def _structured_memory_stats() -> dict:
"""Return StructuredMemoryGraph stats for health and desktop IPC."""
try:
service = current_app.extensions.get("dle_unified_memory_service")
if service is None:
raise RuntimeError("memory_service_not_initialized")
return service.stats()
except Exception as exc: # pylint: disable=broad-except
logger.debug("Structured memory stats unavailable: %s", exc)
return {
"status": "unavailable",
"memory_vertices": 0,
"memory_edges": 0,
"last_recall_timestamp": None,
}
def _db_c_auto_index_enabled(app: Flask) -> bool:
configured = app.config.get("DB_C_AUTO_INDEX_ON_STARTUP")
if configured is not None:
return configured.lower() in {"1", "true", "yes", "on"}
if app.config.get("TESTING"):
return False
return bool(app.config.get("DLE_DESKTOP_MODE"))
def _run_db_c_indexing_background(app: Flask) -> None:
"""Run DB-C knowledge-node indexing inside an app context."""
try:
from scripts.index_knowledge_nodes import index_from_database
with app.app_context():
result = index_from_database(flask_app=app)
logger.info("DB-C knowledge_nodes background index complete: %s", result.to_dict())
except Exception as exc: # pylint: disable=broad-except
logger.warning("DB-C knowledge_nodes background index failed: %s", exc)
def _maybe_start_db_c_indexing(app: Flask) -> None:
"""Trigger DB-C indexing when local desktop Chroma starts empty."""
if not _db_c_auto_index_enabled(app):
return
counts = _chroma_collection_counts()
if counts.get("knowledge_nodes", 0) > 0:
return
thread = Thread(
target=_run_db_c_indexing_background,
args=(app,),
name="db-c-index-knowledge-nodes",
daemon=True,
)
get_application_runtime(app).track_thread(thread)
thread.start()
def _initialize_uskd_memory_graph(app: Flask) -> None:
"""Load the RAM-resident USKD graph from SQL rows, then Neo4j if available."""
try:
from backend.storage import get_graph_store, get_uskd_memory_graph
with app.app_context():
memory_graph = get_uskd_memory_graph()
sql_stats = memory_graph.load_from_database(db.session)
logger.info("USKD memory graph loaded from SQL: %s", sql_stats.to_dict())
graph_store = get_graph_store()
if os.environ.get("USKD_SYNC_NEO4J_ON_STARTUP", "false").lower() in {"1", "true", "yes", "on"}:
try:
from scripts.sync_nodes_to_neo4j import sync
sync_result = sync()
logger.info("USKD SQL→Neo4j startup sync complete: %s", sync_result)
except Exception as sync_exc: # pylint: disable=broad-except
logger.warning("USKD SQL→Neo4j startup sync skipped: %s", sync_exc)
neo4j_stats = memory_graph.load_from_neo4j(graph_store)
if neo4j_stats.node_count:
logger.info("USKD memory graph refreshed from Neo4j: %s", neo4j_stats.to_dict())
except Exception as exc: # pylint: disable=broad-except
logger.warning("USKD memory graph init skipped: %s", exc)
@core_bp.route('/api/v1/csp-report', methods=['POST'])
@api_login_required
def csp_report():
"""Receive and log Content-Security-Policy violation reports."""
try:
payload = request.get_json(force=True, silent=True) or {}
report = payload.get('csp-report', payload)
logger.warning("CSP violation: blocked-uri=%s violated-directive=%s document-uri=%s",
report.get('blocked-uri', ''),
report.get('violated-directive', ''),
report.get('document-uri', ''))
except Exception as exc: # pylint: disable=broad-except
logger.debug("Failed to parse CSP report: %s", exc)
return '', 204
def _config_health() -> dict:
"""Summarize configuration readiness for lightweight health checks."""
secret_key_status = "set" if current_app.secret_key else "missing"
environment = current_app.config.get("DLE_ENVIRONMENT", "production")
return {
"environment": environment,
"secret_key": secret_key_status,
"secret_source": current_app.config.get("DLE_SESSION_SECRET_SOURCE", "missing"),
}
def _database_health() -> dict:
"""Confirm database connectivity and local vector-store readiness."""
try:
with db.engine.connect() as connection:
connection.execute(text("SELECT 1"))
except SQLAlchemyError as exc:
db.session.rollback()
logger.error("Database connectivity check failed", exc_info=exc)
return {"status": "error", "detail": "unavailable"}
return {
"status": "ok",
"chromadb": {
"collections": _chroma_collection_counts(),
},
"redis": {
"ping_ms": _redis_ping_ms(),
},
"object_store": _object_store_bucket_stats(),
"memory": _structured_memory_stats(),
}
def _readiness_payload() -> tuple[dict, int]:
"""Build canonical readiness payload and HTTP status."""
runtime_payload, _ = get_application_runtime().readiness()
config_state = _config_health()
database_state = _database_health()
blockers = list(runtime_payload["blockers"])
blocker_details = dict(runtime_payload.get("blocker_details", {}))