fred-core 3.2.0__tar.gz → 3.4.1__tar.gz

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.
Files changed (171) hide show
  1. {fred_core-3.2.0 → fred_core-3.4.1}/PKG-INFO +4 -2
  2. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/__init__.py +11 -15
  3. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/config_loader.py +41 -1
  4. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/structures.py +20 -1
  5. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/__init__.py +2 -0
  6. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_store.py +0 -10
  7. fred_core-3.4.1/fred_core/documents/tag_models.py +26 -0
  8. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/gcs_filesystem.py +2 -3
  9. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/local_filesystem.py +0 -1
  10. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/minio_filesystem.py +2 -3
  11. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/base_history_store.py +22 -0
  12. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/postgres_history_store.py +15 -0
  13. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/opensearch_kpi_store.py +60 -0
  14. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/log_setup.py +1 -1
  15. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/factory.py +59 -4
  16. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/http_clients.py +0 -1
  17. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/models.py +1 -0
  18. fred_core-3.4.1/fred_core/security/authorization.py +43 -0
  19. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/keycloak/keycloack_admin_client.py +0 -2
  20. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/models.py +0 -17
  21. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/oidc.py +69 -13
  22. fred_core-3.4.1/fred_core/security/permission_catalog.py +90 -0
  23. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/openfga_engine.py +12 -13
  24. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/rebac_engine.py +38 -3
  25. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/schema.fga +22 -0
  26. fred_core-3.4.1/fred_core/security/rebac/schema.fga.json +1 -0
  27. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/structure.py +27 -0
  28. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/opensearch_mapping_validator.py +0 -2
  29. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/__init__.py +14 -0
  30. fred_core-3.4.1/fred_core/tasks/authz.py +143 -0
  31. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/bus.py +80 -22
  32. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/models.py +74 -2
  33. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/orm_models.py +7 -0
  34. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/service.py +64 -11
  35. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/sse.py +40 -16
  36. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/store.py +17 -3
  37. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/metadata_store.py +17 -0
  38. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/team_metatada_models.py +8 -0
  39. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/cli/test_auth.py +0 -1
  40. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_config_loader.py +28 -1
  41. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_fastapi_handlers.py +0 -1
  42. fred_core-3.4.1/fred_core/tests/common/test_structures.py +46 -0
  43. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/filesystem/test_gcs_filesystem.py +0 -1
  44. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/filesystem/test_local_filesystem.py +0 -1
  45. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/kpi/test_http_middleware.py +0 -1
  46. fred_core-3.4.1/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +199 -0
  47. fred_core-3.4.1/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +104 -0
  48. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/logs/test_memory_log_store.py +0 -1
  49. fred_core-3.4.1/fred_core/tests/model/test_anthropic_factory.py +177 -0
  50. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_http_clients.py +1 -2
  51. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -1
  52. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/portable/test_observability.py +0 -1
  53. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/scheduler/test_backend.py +0 -1
  54. fred_core-3.4.1/fred_core/tests/security/test_authorization.py +54 -0
  55. fred_core-3.4.1/fred_core/tests/security/test_oidc_strict.py +101 -0
  56. fred_core-3.4.1/fred_core/tests/security/test_security_profile.py +98 -0
  57. fred_core-3.4.1/fred_core/tests/security/test_service_agent.py +59 -0
  58. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -1
  59. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/store/test_local_content_store.py +0 -1
  60. fred_core-3.4.1/fred_core/tests/tasks/test_authz.py +307 -0
  61. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_bus.py +32 -0
  62. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_reconcile.py +36 -0
  63. fred_core-3.4.1/fred_core/tests/tasks/test_scheduling.py +185 -0
  64. fred_core-3.4.1/fred_core/tests/tasks/test_sse.py +136 -0
  65. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/PKG-INFO +4 -2
  66. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/SOURCES.txt +13 -3
  67. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/requires.txt +3 -1
  68. {fred_core-3.2.0 → fred_core-3.4.1}/pyproject.toml +7 -2
  69. fred_core-3.2.0/fred_core/security/authorization.py +0 -82
  70. fred_core-3.2.0/fred_core/security/authorization_decorator.py +0 -66
  71. fred_core-3.2.0/fred_core/security/rbac.py +0 -134
  72. fred_core-3.2.0/fred_core/security/rebac/schema.fga.json +0 -1
  73. fred_core-3.2.0/fred_core/tests/security/test_authorization.py +0 -176
  74. fred_core-3.2.0/fred_core/tests/security/test_authorization_decorator.py +0 -136
  75. {fred_core-3.2.0 → fred_core-3.4.1}/README.md +0 -0
  76. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/__init__.py +0 -0
  77. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/auth.py +0 -0
  78. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/ui.py +0 -0
  79. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/__init__.py +0 -0
  80. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/config_files.py +0 -0
  81. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/env.py +0 -0
  82. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/fastapi_handlers.py +0 -0
  83. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/lru_cache.py +0 -0
  84. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/team_id.py +0 -0
  85. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/utils.py +0 -0
  86. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_models.py +0 -0
  87. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_structures.py +0 -0
  88. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/postgres_document_store.py +0 -0
  89. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/structures.py +0 -0
  90. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/__init__.py +0 -0
  91. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/history_models.py +0 -0
  92. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/history_schema.py +0 -0
  93. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/__init__.py +0 -0
  94. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/base_kpi_store.py +0 -0
  95. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/base_kpi_writer.py +0 -0
  96. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/http_middleware.py +0 -0
  97. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_factory.py +0 -0
  98. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_phase_metric.py +0 -0
  99. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_process.py +0 -0
  100. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_reader_structures.py +0 -0
  101. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_writer.py +0 -0
  102. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_writer_structures.py +0 -0
  103. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/log_kpi_store.py +0 -0
  104. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/noop_kpi_writer.py +0 -0
  105. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/prometheus_kpi_store.py +0 -0
  106. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/__init__.py +0 -0
  107. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/base_log_store.py +0 -0
  108. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/log_structures.py +0 -0
  109. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/memory_log_store.py +0 -0
  110. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/null_log_store.py +0 -0
  111. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/opensearch_log_store.py +0 -0
  112. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/models/__init__.py +0 -0
  113. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/models/base.py +0 -0
  114. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/portable/__init__.py +0 -0
  115. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/portable/observability.py +0 -0
  116. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/py.typed +0 -0
  117. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/__init__.py +0 -0
  118. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/backend.py +0 -0
  119. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/scheduler_structures.py +0 -0
  120. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/temporal_client_provider.py +0 -0
  121. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/__init__.py +0 -0
  122. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/backend_to_backend_auth.py +0 -0
  123. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/keycloak/__init__.py +0 -0
  124. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/outbound.py +0 -0
  125. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/noop_engine.py +0 -0
  126. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/openfga_schema.py +0 -0
  127. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/rebac_factory.py +0 -0
  128. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/whitelist_access_control/access_control.py +0 -0
  129. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/__init__.py +0 -0
  130. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/session_schema.py +0 -0
  131. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/__init__.py +0 -0
  132. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/base_session_store.py +0 -0
  133. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/postgres_session_store.py +0 -0
  134. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/session_models.py +0 -0
  135. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/__init__.py +0 -0
  136. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/alembic_env.py +0 -0
  137. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/async_session.py +0 -0
  138. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/base_sql.py +0 -0
  139. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/mixin.py +0 -0
  140. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/__init__.py +0 -0
  141. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/base_content_store.py +0 -0
  142. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/local_content_store.py +0 -0
  143. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/minio_content_store.py +0 -0
  144. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/vector_search.py +0 -0
  145. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/workflow_control.py +0 -0
  146. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/__init__.py +0 -0
  147. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/__init__.py +0 -0
  148. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/cli/test_ui.py +0 -0
  149. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_env.py +0 -0
  150. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_log_setup.py +0 -0
  151. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_lru_cache.py +0 -0
  152. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/integration/__init__.py +0 -0
  153. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/integration/test_rebac.py +0 -0
  154. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
  155. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_embedding_factory.py +0 -0
  156. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/__init__.py +0 -0
  157. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
  158. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_rebac_engine_team_helpers.py +0 -0
  159. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
  160. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/__init__.py +0 -0
  161. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_models.py +0 -0
  162. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/test_log_kpi_store.py +0 -0
  163. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
  164. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/__init__.py +0 -0
  165. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/__init__.py +0 -0
  166. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/base_user_store.py +0 -0
  167. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/postgres_user_store.py +0 -0
  168. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/user_models.py +0 -0
  169. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/dependency_links.txt +0 -0
  170. {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/top_level.txt +0 -0
  171. {fred_core-3.2.0 → fred_core-3.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-core
3
- Version: 3.2.0
3
+ Version: 3.4.1
4
4
  Summary: Core shared utilities for Fred backends (config, storage, security, and runtime helpers).
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -12,7 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Operating System :: OS Independent
13
13
  Requires-Python: >=3.12
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: fastapi==0.116.1
15
+ Requires-Dist: fastapi==0.139.0
16
+ Requires-Dist: starlette>=1.3.1
16
17
  Requires-Dist: prometheus-client>=0.20.5
17
18
  Requires-Dist: pyjwt<3.0.0,>=2.10.1
18
19
  Requires-Dist: psycopg[binary]>=3.1.0
@@ -28,6 +29,7 @@ Requires-Dist: langchain-google-genai>=4.2.1
28
29
  Requires-Dist: langchain-ollama>=1.0.1
29
30
  Requires-Dist: langchain-mistralai>=1.1.2
30
31
  Requires-Dist: langchain-google-vertexai>=3.2.2
32
+ Requires-Dist: langchain-anthropic>=0.3.0
31
33
  Requires-Dist: langchain-openai>=1.1.14
32
34
  Requires-Dist: azure-identity>=1.25.0
33
35
  Requires-Dist: authzed>=1.22.1
@@ -61,15 +61,7 @@ from fred_core.model.models import ModelProvider
61
61
  from fred_core.security.authorization import (
62
62
  NO_AUTHZ_CHECK_USER,
63
63
  TODO_PASS_REAL_USER,
64
- Action,
65
- AuthorizationError,
66
- Resource,
67
- authorize_or_raise,
68
- is_authorized,
69
- require_admin,
70
- require_task_access,
71
64
  )
72
- from fred_core.security.authorization_decorator import authorize
73
65
  from fred_core.security.backend_to_backend_auth import (
74
66
  M2MAuthConfig,
75
67
  M2MBearerAuth,
@@ -80,6 +72,11 @@ from fred_core.security.keycloak.keycloack_admin_client import (
80
72
  KeycloackDisabled,
81
73
  create_keycloak_admin,
82
74
  )
75
+ from fred_core.security.models import (
76
+ Action,
77
+ AuthorizationError,
78
+ Resource,
79
+ )
83
80
  from fred_core.security.oidc import (
84
81
  decode_jwt,
85
82
  get_current_user,
@@ -91,10 +88,11 @@ from fred_core.security.oidc import (
91
88
  split_realm_url,
92
89
  )
93
90
  from fred_core.security.outbound import BearerAuth, ClientCredentialsProvider
94
- from fred_core.security.rbac import RBACProvider
91
+ from fred_core.security.permission_catalog import list_display_permissions
95
92
  from fred_core.security.rebac.openfga_engine import OpenFgaRebacEngine
96
93
  from fred_core.security.rebac.rebac_engine import (
97
94
  ORGANIZATION_ID,
95
+ SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS,
98
96
  AgentPermission,
99
97
  DocumentPermission,
100
98
  OrganizationPermission,
@@ -115,6 +113,7 @@ from fred_core.security.structure import (
115
113
  RebacConfiguration,
116
114
  SecurityConfiguration,
117
115
  UserSecurity,
116
+ is_service_agent,
118
117
  )
119
118
  from fred_core.session import SessionSchema
120
119
  from fred_core.session.stores import BaseSessionStore, PostgresSessionStore
@@ -164,15 +163,12 @@ __all__ = [
164
163
  "GcsFilesystem",
165
164
  "FilesystemResourceInfoResult",
166
165
  "FilesystemResourceInfo",
167
- "RBACProvider",
168
- "require_admin",
169
- "require_task_access",
166
+ "is_service_agent",
167
+ "SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS",
168
+ "list_display_permissions",
170
169
  "Action",
171
170
  "Resource",
172
171
  "AuthorizationError",
173
- "is_authorized",
174
- "authorize_or_raise",
175
- "authorize",
176
172
  "oauth2_scheme",
177
173
  "ClientCredentialsProvider",
178
174
  "BearerAuth",
@@ -14,15 +14,49 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import sys
17
18
  from typing import Callable, TypeVar
18
19
 
19
20
  import yaml
21
+ from pydantic import ValidationError
20
22
 
21
23
  from .config_files import ConfigFiles
22
24
 
23
25
  TConfig = TypeVar("TConfig")
24
26
 
25
27
 
28
+ def _render_config_error_banner(config_file: str, error: Exception) -> None:
29
+ """Print a loud, unmissable configuration-error banner to stderr.
30
+
31
+ A misconfigured service must not start silently and fail later with an
32
+ opaque error inside a request handler. We surface the root cause in red at
33
+ startup. Colours are emitted only on a TTY so log files stay clean.
34
+ """
35
+ use_colour = sys.stderr.isatty()
36
+ red = "\033[1;31m" if use_colour else ""
37
+ reset = "\033[0m" if use_colour else ""
38
+ bar = "=" * 78
39
+
40
+ if isinstance(error, ValidationError):
41
+ details = "\n".join(
42
+ f" - {' -> '.join(str(p) for p in err['loc']) or '(root)'}: {err['msg']}"
43
+ for err in error.errors()
44
+ )
45
+ else:
46
+ details = f" - {error}"
47
+
48
+ print(
49
+ f"\n{red}{bar}\n"
50
+ f" CONFIGURATION ERROR — refusing to start\n"
51
+ f" file: {config_file}\n"
52
+ f"{bar}{reset}\n"
53
+ f"{details}\n"
54
+ f"{red}{bar}{reset}\n",
55
+ file=sys.stderr,
56
+ flush=True,
57
+ )
58
+
59
+
26
60
  def parse_yaml_mapping_file(config_file: str) -> dict:
27
61
  """Load a YAML file and ensure it is a non-empty mapping."""
28
62
  with open(config_file, encoding="utf-8") as file:
@@ -42,7 +76,13 @@ def load_configuration_with_config_files(
42
76
  """Load env + config path using ConfigFiles and parse via callback."""
43
77
  config_files.load_environment(dotenv_path)
44
78
  config_file = config_files.resolve_config_file_path()
45
- configuration = parser(config_file)
79
+ try:
80
+ configuration = parser(config_file)
81
+ except (ValidationError, ValueError) as exc:
82
+ # Render the root cause in red and stop, rather than letting an opaque
83
+ # traceback (or a deferred runtime 401) bury what is wrong.
84
+ _render_config_error_banner(config_file, exc)
85
+ raise SystemExit(1) from exc
46
86
  config_files.mark_config_loaded(config_file)
47
87
  return configuration
48
88
 
@@ -16,7 +16,7 @@ import os
16
16
  from enum import Enum
17
17
  from typing import Annotated, Any, Dict, Literal, Optional, Union
18
18
 
19
- from pydantic import BaseModel, Field
19
+ from pydantic import BaseModel, Field, model_validator
20
20
 
21
21
 
22
22
  class OwnerFilter(str, Enum):
@@ -81,6 +81,25 @@ class OpenSearchStoreConfig(BaseModel):
81
81
  secure: bool = Field(default=False, description="Use TLS (https)")
82
82
  verify_certs: bool = Field(default=False, description="Verify TLS certs")
83
83
 
84
+ @model_validator(mode="after")
85
+ def _require_password(self) -> "OpenSearchStoreConfig":
86
+ """Fail fast at config load if OpenSearch is configured without credentials.
87
+
88
+ Reaching this validator means a ``storage.opensearch`` block is present in
89
+ the active configuration, so the service genuinely depends on OpenSearch.
90
+ A missing password only surfaces later as an opaque HTTP 401 deep inside a
91
+ request handler — we convert it into an actionable startup failure instead.
92
+ """
93
+ if not self.password:
94
+ raise ValueError(
95
+ f"OpenSearch is configured (host={self.host!r}, username="
96
+ f"{self.username!r}) but no password was provided. "
97
+ "Set the OPENSEARCH_PASSWORD environment variable (in your .env) "
98
+ "to the OpenSearch password for this user, or remove the "
99
+ "storage.opensearch block if OpenSearch is not used."
100
+ )
101
+ return self
102
+
84
103
 
85
104
  class OpenSearchIndexConfig(BaseModel):
86
105
  type: Literal["opensearch"]
@@ -38,6 +38,7 @@ from fred_core.documents.document_structures import (
38
38
  Tagging,
39
39
  )
40
40
  from fred_core.documents.postgres_document_store import PostgresDocumentMetadataStore
41
+ from fred_core.documents.tag_models import TagRow
41
42
 
42
43
  __all__ = [
43
44
  # Pydantic models / enums
@@ -64,4 +65,5 @@ __all__ = [
64
65
  "DocumentMetadataDeserializationError",
65
66
  "DocumentMetadataRow",
66
67
  "PostgresDocumentMetadataStore",
68
+ "TagRow",
67
69
  ]
@@ -24,8 +24,6 @@ from fred_core.documents.document_structures import DocumentMetadata
24
24
  class DocumentMetadataDeserializationError(Exception):
25
25
  """Raised when document metadata cannot be parsed correctly due to invalid fields or enum mismatches."""
26
26
 
27
- pass
28
-
29
27
 
30
28
  class BaseDocumentMetadataStore:
31
29
  """
@@ -37,7 +35,6 @@ class BaseDocumentMetadataStore:
37
35
  @abstractmethod
38
36
  async def count_all(self, session: AsyncSession | None = None) -> int:
39
37
  """Return the total number of document metadata records in the store."""
40
- pass
41
38
 
42
39
  @abstractmethod
43
40
  async def get_all_metadata(
@@ -50,7 +47,6 @@ class BaseDocumentMetadataStore:
50
47
  - Keys are metadata field names (e.g., "source_tag", "tags")
51
48
  - Values are filter values (exact match). Lists are interpreted as 'terms'.
52
49
  """
53
- pass
54
50
 
55
51
  @abstractmethod
56
52
  async def get_metadata_by_uid(
@@ -61,7 +57,6 @@ class BaseDocumentMetadataStore:
61
57
 
62
58
  :raises DocumentMetadataDeserializationError: if stored data is malformed.
63
59
  """
64
- pass
65
60
 
66
61
  async def get_metadata_by_uids(
67
62
  self, document_uids: list[str], session: AsyncSession | None = None
@@ -88,7 +83,6 @@ class BaseDocumentMetadataStore:
88
83
  self, tag_id: str, session: AsyncSession | None = None
89
84
  ) -> List[DocumentMetadata]:
90
85
  """Return all metadata entries that are tagged with a specific tag ID."""
91
- pass
92
86
 
93
87
  async def browse_metadata_in_tag(
94
88
  self,
@@ -107,7 +101,6 @@ class BaseDocumentMetadataStore:
107
101
  self, source_tag: str, session: AsyncSession | None = None
108
102
  ) -> List[DocumentMetadata]:
109
103
  """Return all metadata entries originating from a specific pull source."""
110
- pass
111
104
 
112
105
  @abstractmethod
113
106
  async def save_metadata(
@@ -121,7 +114,6 @@ class BaseDocumentMetadataStore:
121
114
 
122
115
  :raises ValueError: if 'document_uid' is missing.
123
116
  """
124
- pass
125
117
 
126
118
  @abstractmethod
127
119
  async def delete_metadata(
@@ -132,9 +124,7 @@ class BaseDocumentMetadataStore:
132
124
 
133
125
  :raises ValueError: if 'document_uid' is missing.
134
126
  """
135
- pass
136
127
 
137
128
  @abstractmethod
138
129
  async def clear(self, session: AsyncSession | None = None) -> None:
139
130
  """Delete all metadata records from the store. Destructive — dev/test only."""
140
- pass
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import String
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from fred_core.models.base import Base, JsonColumn, TimestampColumn
9
+
10
+
11
+ class TagRow(Base):
12
+ """ORM model for the ``tag`` table — shared between knowledge-flow and the importer."""
13
+
14
+ __tablename__ = "tag"
15
+
16
+ tag_id: Mapped[str] = mapped_column(String, primary_key=True)
17
+ created_at: Mapped[datetime | None] = mapped_column(TimestampColumn, nullable=True)
18
+ updated_at: Mapped[datetime | None] = mapped_column(
19
+ TimestampColumn, index=True, nullable=True
20
+ )
21
+ owner_id: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
22
+ name: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
23
+ path: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
24
+ description: Mapped[str | None] = mapped_column(String, nullable=True)
25
+ type: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
26
+ doc: Mapped[dict | None] = mapped_column(JsonColumn, nullable=True)
@@ -17,14 +17,13 @@ import re
17
17
  import time
18
18
  from typing import List, Optional, Set
19
19
 
20
- from google.cloud import storage
21
- from google.cloud.exceptions import NotFound
22
-
23
20
  from fred_core.filesystem.structures import (
24
21
  BaseFilesystem,
25
22
  FilesystemResourceInfo,
26
23
  FilesystemResourceInfoResult,
27
24
  )
25
+ from google.cloud import storage
26
+ from google.cloud.exceptions import NotFound
28
27
 
29
28
  logger = logging.getLogger(__name__)
30
29
 
@@ -20,7 +20,6 @@ from pathlib import Path
20
20
  from typing import List
21
21
 
22
22
  import aiofiles
23
-
24
23
  from fred_core.filesystem.structures import (
25
24
  BaseFilesystem,
26
25
  FilesystemResourceInfo,
@@ -18,14 +18,13 @@ from io import BytesIO
18
18
  from typing import List, Set
19
19
  from urllib.parse import urlparse
20
20
 
21
- from minio import Minio
22
- from minio.deleteobjects import DeleteObject
23
-
24
21
  from fred_core.filesystem.structures import (
25
22
  BaseFilesystem,
26
23
  FilesystemResourceInfo,
27
24
  FilesystemResourceInfoResult,
28
25
  )
26
+ from minio import Minio
27
+ from minio.deleteobjects import DeleteObject
29
28
 
30
29
  logger = logging.getLogger(__name__)
31
30
 
@@ -148,6 +148,22 @@ class BaseHistoryStore(ABC):
148
148
  """
149
149
  raise NotImplementedError
150
150
 
151
+ async def session_exists(
152
+ self,
153
+ session_id: str,
154
+ ) -> bool:
155
+ """
156
+ Return True iff any history row exists for ``session_id`` (any owner).
157
+
158
+ Why this exists:
159
+ - paired with :meth:`session_belongs_to_user`, this lets a caller enforce a
160
+ private-per-owner policy: a session that EXISTS but does not belong to the
161
+ authenticated user must be refused (RUNTIME-07 rev. 2, finding F-C). It
162
+ distinguishes a brand-new session (allow — the caller becomes owner) from
163
+ another user's session (deny).
164
+ """
165
+ raise NotImplementedError
166
+
151
167
 
152
168
  class NoOpHistoryStore(BaseHistoryStore):
153
169
  """
@@ -199,3 +215,9 @@ class NoOpHistoryStore(BaseHistoryStore):
199
215
  user_id: str,
200
216
  ) -> bool:
201
217
  return False
218
+
219
+ async def session_exists(
220
+ self,
221
+ session_id: str,
222
+ ) -> bool:
223
+ return False
@@ -368,6 +368,21 @@ class PostgresHistoryStore(BaseHistoryStore):
368
368
  )
369
369
  return (result.scalar() or 0) > 0
370
370
 
371
+ async def session_exists(
372
+ self,
373
+ session_id: str,
374
+ session: AsyncSession | None = None,
375
+ ) -> bool:
376
+ """Return True iff any history row exists for ``session_id`` (any owner)."""
377
+ await self._ensure_tables()
378
+ async with use_session(self._sessions, session) as s:
379
+ result = await s.execute(
380
+ select(func.count()).where(
381
+ SessionHistoryRow.session_id == session_id,
382
+ )
383
+ )
384
+ return (result.scalar() or 0) > 0
385
+
371
386
  # ------------------------------------------------------------------
372
387
  # Rank helper
373
388
  # ------------------------------------------------------------------
@@ -64,6 +64,10 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
64
64
  "env": {"type": "keyword"},
65
65
  "cluster": {"type": "keyword"},
66
66
  "user_id": {"type": "keyword"},
67
+ # Conversation id carried by every session-scoped KPI row
68
+ # (react/graph phase, tool). Explicit keyword so the RGPD
69
+ # erasure `update_by_query` can `term`-match it (CTRLP-12 A3).
70
+ "session_id": {"type": "keyword"},
67
71
  "exchange_id": {"type": "keyword"},
68
72
  "agent_id": {"type": "keyword"},
69
73
  "agent_step": {"type": "keyword"},
@@ -170,6 +174,11 @@ class OpenSearchKPIStore(BaseKPIStore):
170
174
  logger.info("[OPENSEARCH][KPI] created index '%s'", self.index)
171
175
  else:
172
176
  logger.info("[OPENSEARCH][KPI] index '%s' already exists.", self.index)
177
+ # CTRLP-12: session_id was added to KPI_INDEX_MAPPING for RGPD
178
+ # conversation erasure. Existing indices predate it, so add it
179
+ # additively (put_mapping) before validation — otherwise startup
180
+ # fails with "Missing nested field: 'dims.session_id'".
181
+ self._ensure_dim_mapping("session_id", {"type": "keyword"})
173
182
  self._ensure_dim_mapping("service", {"type": "keyword"})
174
183
  self._ensure_dim_mapping("team_id", {"type": "keyword"})
175
184
  self._ensure_dim_mapping("agent_instance_id", {"type": "keyword"})
@@ -234,6 +243,57 @@ class OpenSearchKPIStore(BaseKPIStore):
234
243
  logger.error("[OPENSEARCH][KPI] bulk_index failed: %s", e)
235
244
  raise
236
245
 
246
+ # -- erasure (RGPD) --------------------------------------------------------
247
+ def anonymise_for_session(self, session_id: str) -> int:
248
+ """Anonymise — not delete — every KPI row of one conversation (CTRLP-12 A3).
249
+
250
+ RGPD default per RFC §3.3: KPI events are an analytics *aggregate*, not
251
+ conversation content, so erasure severs the link to a person rather than
252
+ dropping the rows. An OpenSearch `update_by_query` (the same primitive the
253
+ vector store uses for `delete_by_query`) nulls the direct identifiers on
254
+ the session's rows: `dims.session_id`, `dims.user_id`, `dims.exchange_id`
255
+ and — for any generic session-scoped analytics row — `dims.scope_id`.
256
+ Aggregate counts are untouched — the rows survive, now anonymous. Returns
257
+ the number of rows anonymised.
258
+
259
+ A session's rows are matched by `dims.session_id == session_id`, the dim
260
+ every session-scoped KPI row actually carries (react/graph `phase_latency`
261
+ and `tool_*` all set `dims.session_id`; see the runtime KPI emitters). A
262
+ pure aggregate with no `session_id` (e.g. `llm.call_latency_ms` emitted
263
+ under a system actor) carries no personal identifier and is correctly out
264
+ of per-conversation scope. Idempotent: once `session_id` is nulled the row
265
+ no longer matches, so a second erase updates zero.
266
+ """
267
+ body: Dict[str, Any] = {
268
+ "query": {"bool": {"filter": [{"term": {"dims.session_id": session_id}}]}},
269
+ "script": {
270
+ "lang": "painless",
271
+ "source": (
272
+ "if (ctx._source.dims != null) "
273
+ "{ for (f in params.fields) { ctx._source.dims.remove(f); } }"
274
+ ),
275
+ "params": {
276
+ "fields": ["session_id", "scope_id", "user_id", "exchange_id"]
277
+ },
278
+ },
279
+ }
280
+ try:
281
+ resp = self.client.update_by_query(
282
+ index=self.index,
283
+ body=body,
284
+ params={"conflicts": "proceed", "refresh": "true"},
285
+ )
286
+ updated = int(resp.get("updated", 0))
287
+ logger.info(
288
+ "[OPENSEARCH][KPI] anonymised %s row(s) for session_id=%s",
289
+ updated,
290
+ session_id,
291
+ )
292
+ return updated
293
+ except OpenSearchException as e:
294
+ logger.error("[OPENSEARCH][KPI] anonymise_for_session failed: %s", e)
295
+ raise
296
+
237
297
  # -- reads -----------------------------------------------------------------
238
298
  def query(self, q: KPIQuery) -> KPIQueryResult:
239
299
  logger.info(
@@ -81,7 +81,7 @@ class StoreEmitHandler(logging.Handler):
81
81
  payload = json.loads(raw)
82
82
  except Exception:
83
83
  print("Log record is not JSON: %s", raw)
84
- pass # formatter should be JSON, but we tolerate plain text
84
+ # formatter should be JSON, but we tolerate plain text
85
85
 
86
86
  if payload:
87
87
  mapped_level = LEVEL_MAP.get(payload.get("level"), record.levelname)
@@ -13,6 +13,9 @@ from collections.abc import AsyncGenerator, Generator
13
13
  from typing import Any, Dict, Iterable, Optional, Protocol, Sequence, Type, cast
14
14
 
15
15
  import httpx
16
+ from fred_core.common import ModelConfiguration
17
+ from fred_core.model.http_clients import get_shared_stack, strip_transport_settings
18
+ from fred_core.model.models import ModelProvider
16
19
  from langchain_core.embeddings import Embeddings as LCEmbeddings
17
20
  from langchain_core.language_models.chat_models import BaseChatModel
18
21
  from langchain_core.output_parsers import PydanticOutputParser
@@ -26,10 +29,6 @@ from langchain_openai import (
26
29
  )
27
30
  from pydantic import BaseModel
28
31
 
29
- from fred_core.common import ModelConfiguration
30
- from fred_core.model.http_clients import get_shared_stack, strip_transport_settings
31
- from fred_core.model.models import ModelProvider
32
-
33
32
  logger = logging.getLogger(__name__)
34
33
 
35
34
  # ---------------------------------------------------------------------------
@@ -572,6 +571,61 @@ def get_model(cfg: Optional[ModelConfiguration]) -> BaseChatModel:
572
571
  **settings,
573
572
  )
574
573
 
574
+ # --- Provider: Anthropic (native + Synapse Gateway) ---
575
+ if provider == ModelProvider.ANTHROPIC.value:
576
+ if not cfg.name:
577
+ raise ValueError(
578
+ "Anthropic chat requires 'name' (e.g., claude-sonnet-4-5)."
579
+ )
580
+ # Accept both ANTHROPIC_API_KEY (standard) and ANTHROPIC_AUTH_TOKEN (legacy/Thales env)
581
+ api_key = os.getenv("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_AUTH_TOKEN")
582
+ if not api_key:
583
+ raise ValueError(
584
+ "Missing required environment variable: ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN)"
585
+ )
586
+ base_url = settings.pop("base_url", None)
587
+
588
+ # --- Proxy bypass patch (WSL / corporate proxy environments) ---
589
+ # WSL sets lowercase no_proxy in the shell before the process starts; python-dotenv
590
+ # does not override already-set env vars by default, so our .env value is ignored.
591
+ # httpx prefers lowercase no_proxy over NO_PROXY, so we must patch it here at the
592
+ # Python level before the Anthropic SDK constructs its internal httpx client.
593
+ _effective_base = base_url or os.getenv("ANTHROPIC_BASE_URL") or ""
594
+ if _effective_base:
595
+ try:
596
+ from urllib.parse import urlparse as _urlparse
597
+
598
+ _host = _urlparse(_effective_base).hostname or ""
599
+ if _host:
600
+ _no_proxy = os.environ.get("no_proxy", "")
601
+ _parts = {h.strip() for h in _no_proxy.split(",") if h.strip()}
602
+ if _host not in _parts:
603
+ _parts.add(_host)
604
+ os.environ["no_proxy"] = ",".join(sorted(_parts))
605
+ logger.info(
606
+ "[MODEL][ANTHROPIC] Patched no_proxy: added %s for direct connection",
607
+ _host,
608
+ )
609
+ except Exception: # nosec B110 — best-effort proxy patch; must not block model construction
610
+ pass
611
+
612
+ _info_provider(cfg, settings)
613
+ logger.info(
614
+ "[MODEL][ANTHROPIC] Constructing ChatAnthropic model=%s base_url=%s",
615
+ cfg.name,
616
+ base_url or "(default, from ANTHROPIC_BASE_URL env)",
617
+ )
618
+ try:
619
+ from langchain_anthropic import ChatAnthropic
620
+ except ImportError as e:
621
+ raise ImportError(
622
+ "Provider 'anthropic' requires package 'langchain-anthropic'."
623
+ ) from e
624
+ ctor_kwargs: Dict[str, Any] = {"model": cfg.name, "anthropic_api_key": api_key}
625
+ if base_url:
626
+ ctor_kwargs["base_url"] = base_url
627
+ return ChatAnthropic(**ctor_kwargs, **settings)
628
+
575
629
  raise ValueError(f"Unsupported chat provider: {provider}")
576
630
 
577
631
 
@@ -777,6 +831,7 @@ def get_structured_chain(schema: Type[BaseModel], model_config: ModelConfigurati
777
831
  ModelProvider.AZURE_APIM.value,
778
832
  ModelProvider.VERTEX_AI.value,
779
833
  ModelProvider.VERTEX_AI_MODEL_GARDEN.value,
834
+ ModelProvider.ANTHROPIC.value,
780
835
  }:
781
836
  try:
782
837
  structured = model.with_structured_output(schema, method="function_calling")
@@ -14,7 +14,6 @@ from dataclasses import dataclass
14
14
  from typing import Any, Dict, Optional, Tuple
15
15
 
16
16
  import httpx
17
-
18
17
  from fred_core.common import ModelConfiguration
19
18
 
20
19
  logger = logging.getLogger(__name__)
@@ -18,6 +18,7 @@ from enum import Enum
18
18
  class ModelProvider(Enum):
19
19
  """Enumeration of model providers available in the system."""
20
20
 
21
+ ANTHROPIC = "anthropic"
21
22
  AZURE_APIM = "azure-apim"
22
23
  AZURE_OPENAI = "azure-openai"
23
24
  OLLAMA = "ollama"
@@ -0,0 +1,43 @@
1
+ # Copyright Thales 2025
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Residual authorization helpers after the RBAC→ReBAC migration (AUTHZ-01).
16
+
17
+ Role-based authorization (RBACProvider / @authorize / authorize_or_raise /
18
+ is_authorized / require_admin) has been removed — all endpoint authorization is now
19
+ ReBAC (OpenFGA). Task access authorization (list / stream / cancel) lives in its own
20
+ canonical owner, ``fred_core.tasks.authz`` (CTRLP-12), and is ReBAC-based; this module
21
+ only retains the internal service-principal users below.
22
+ """
23
+
24
+ from fred_core.security.structure import KeycloakUser
25
+
26
+ # Fake user to use when a function requires a user but we don't have one yet
27
+ # but should have one when authorization is added to the service.
28
+ # This is a temporary workaround and should be replaced with a real user.
29
+ TODO_PASS_REAL_USER = KeycloakUser(
30
+ uid="internal-admin-todo",
31
+ username="internal admin - todo",
32
+ email="internal-admin-todo@localhost",
33
+ roles=["admin"],
34
+ )
35
+
36
+ # Internal admin user to use for system operations that must always succeed
37
+ # and should not be subject to authorization checks.
38
+ NO_AUTHZ_CHECK_USER = KeycloakUser(
39
+ uid="internal-admin",
40
+ username="internal admin",
41
+ email="internal-admin@localhost",
42
+ roles=["admin"],
43
+ )
@@ -29,8 +29,6 @@ class KeycloackDisabled:
29
29
  to let know the caller it must handle this case.
30
30
  """
31
31
 
32
- ...
33
-
34
32
 
35
33
  def create_keycloak_admin(
36
34
  m2m_security: M2MSecurity,