fred-core 3.4.0__tar.gz → 3.4.2__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 (173) hide show
  1. {fred_core-3.4.0 → fred_core-3.4.2}/PKG-INFO +4 -2
  2. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/__init__.py +9 -15
  3. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/__init__.py +2 -1
  4. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/fastapi_handlers.py +5 -4
  5. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/structures.py +5 -0
  6. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/document_store.py +0 -10
  7. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/filesystem/gcs_filesystem.py +2 -3
  8. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/filesystem/local_filesystem.py +0 -1
  9. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/filesystem/minio_filesystem.py +2 -3
  10. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_phase_metric.py +1 -1
  11. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_writer.py +1 -3
  12. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_writer_structures.py +0 -1
  13. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/opensearch_kpi_store.py +88 -2
  14. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/log_setup.py +1 -1
  15. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/model/factory.py +59 -4
  16. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/model/http_clients.py +0 -1
  17. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/model/models.py +1 -0
  18. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/portable/observability.py +0 -4
  19. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/scheduler/temporal_client_provider.py +19 -2
  20. fred_core-3.4.2/fred_core/security/authorization.py +43 -0
  21. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/keycloak/keycloack_admin_client.py +0 -2
  22. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/models.py +0 -18
  23. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/oidc.py +0 -3
  24. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/rebac/noop_engine.py +11 -0
  25. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/rebac/openfga_engine.py +82 -18
  26. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/rebac/rebac_engine.py +113 -111
  27. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/rebac/rebac_factory.py +2 -2
  28. fred_core-3.4.2/fred_core/security/rebac/schema.fga +215 -0
  29. fred_core-3.4.2/fred_core/security/rebac/schema.fga.json +1 -0
  30. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/structure.py +34 -1
  31. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/opensearch_mapping_validator.py +0 -2
  32. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/__init__.py +8 -0
  33. fred_core-3.4.2/fred_core/tasks/authz.py +143 -0
  34. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/bus.py +80 -22
  35. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/models.py +58 -2
  36. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/orm_models.py +7 -0
  37. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/service.py +55 -13
  38. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/sse.py +40 -16
  39. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/store.py +17 -3
  40. fred_core-3.4.2/fred_core/teams/metadata_store.py +335 -0
  41. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/teams/team_metatada_models.py +16 -0
  42. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/cli/test_auth.py +0 -1
  43. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_config_loader.py +0 -1
  44. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_fastapi_handlers.py +1 -2
  45. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_structures.py +1 -2
  46. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/filesystem/test_gcs_filesystem.py +0 -1
  47. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/filesystem/test_local_filesystem.py +0 -1
  48. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/integration/test_rebac.py +514 -141
  49. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/kpi/test_http_middleware.py +0 -1
  50. fred_core-3.4.2/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +199 -0
  51. fred_core-3.4.2/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +104 -0
  52. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/logs/test_memory_log_store.py +0 -1
  53. fred_core-3.4.2/fred_core/tests/model/test_anthropic_factory.py +177 -0
  54. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/model/test_http_clients.py +1 -2
  55. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -1
  56. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/portable/test_observability.py +1 -4
  57. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/scheduler/test_backend.py +0 -1
  58. fred_core-3.4.2/fred_core/tests/scheduler/test_temporal_client_provider.py +84 -0
  59. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/test_oidc_strict.py +27 -0
  60. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_engine_team_helpers.py +99 -10
  61. fred_core-3.4.2/fred_core/tests/security/test_rebac_schema_authz05.py +186 -0
  62. fred_core-3.4.2/fred_core/tests/security/test_service_agent.py +57 -0
  63. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -1
  64. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/store/test_local_content_store.py +0 -1
  65. fred_core-3.4.2/fred_core/tests/tasks/test_authz.py +307 -0
  66. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/tasks/test_bus.py +32 -0
  67. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/tasks/test_reconcile.py +36 -0
  68. fred_core-3.4.2/fred_core/tests/tasks/test_scheduling.py +185 -0
  69. fred_core-3.4.2/fred_core/tests/tasks/test_sse.py +136 -0
  70. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core.egg-info/PKG-INFO +4 -2
  71. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core.egg-info/SOURCES.txt +10 -4
  72. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core.egg-info/requires.txt +3 -1
  73. {fred_core-3.4.0 → fred_core-3.4.2}/pyproject.toml +7 -2
  74. fred_core-3.4.0/fred_core/security/authorization.py +0 -82
  75. fred_core-3.4.0/fred_core/security/authorization_decorator.py +0 -66
  76. fred_core-3.4.0/fred_core/security/rbac.py +0 -134
  77. fred_core-3.4.0/fred_core/security/rebac/schema.fga +0 -146
  78. fred_core-3.4.0/fred_core/security/rebac/schema.fga.json +0 -1
  79. fred_core-3.4.0/fred_core/teams/metadata_store.py +0 -189
  80. fred_core-3.4.0/fred_core/tests/security/test_authorization.py +0 -176
  81. fred_core-3.4.0/fred_core/tests/security/test_authorization_decorator.py +0 -136
  82. {fred_core-3.4.0 → fred_core-3.4.2}/README.md +0 -0
  83. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/cli/__init__.py +0 -0
  84. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/cli/auth.py +0 -0
  85. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/cli/ui.py +0 -0
  86. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/config_files.py +0 -0
  87. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/config_loader.py +0 -0
  88. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/env.py +0 -0
  89. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/lru_cache.py +0 -0
  90. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/team_id.py +0 -0
  91. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/common/utils.py +0 -0
  92. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/__init__.py +0 -0
  93. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/document_models.py +0 -0
  94. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/document_structures.py +0 -0
  95. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/postgres_document_store.py +0 -0
  96. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/documents/tag_models.py +0 -0
  97. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/filesystem/structures.py +0 -0
  98. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/history/__init__.py +0 -0
  99. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/history/base_history_store.py +0 -0
  100. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/history/history_models.py +0 -0
  101. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/history/history_schema.py +0 -0
  102. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/history/postgres_history_store.py +0 -0
  103. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/__init__.py +0 -0
  104. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/base_kpi_store.py +0 -0
  105. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/base_kpi_writer.py +0 -0
  106. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/http_middleware.py +0 -0
  107. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_factory.py +0 -0
  108. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_process.py +0 -0
  109. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/kpi_reader_structures.py +0 -0
  110. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/log_kpi_store.py +0 -0
  111. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/noop_kpi_writer.py +0 -0
  112. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/kpi/prometheus_kpi_store.py +0 -0
  113. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/__init__.py +0 -0
  114. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/base_log_store.py +0 -0
  115. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/log_structures.py +0 -0
  116. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/memory_log_store.py +0 -0
  117. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/null_log_store.py +0 -0
  118. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/logs/opensearch_log_store.py +0 -0
  119. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/models/__init__.py +0 -0
  120. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/models/base.py +0 -0
  121. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/portable/__init__.py +0 -0
  122. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/py.typed +0 -0
  123. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/scheduler/__init__.py +0 -0
  124. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/scheduler/backend.py +0 -0
  125. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/scheduler/scheduler_structures.py +0 -0
  126. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/__init__.py +0 -0
  127. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/backend_to_backend_auth.py +0 -0
  128. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/keycloak/__init__.py +0 -0
  129. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/outbound.py +0 -0
  130. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/rebac/openfga_schema.py +0 -0
  131. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/security/whitelist_access_control/access_control.py +0 -0
  132. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/__init__.py +0 -0
  133. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/session_schema.py +0 -0
  134. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/stores/__init__.py +0 -0
  135. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/stores/base_session_store.py +0 -0
  136. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/stores/postgres_session_store.py +0 -0
  137. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/session/stores/session_models.py +0 -0
  138. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/sql/__init__.py +0 -0
  139. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/sql/alembic_env.py +0 -0
  140. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/sql/async_session.py +0 -0
  141. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/sql/base_sql.py +0 -0
  142. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/sql/mixin.py +0 -0
  143. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/__init__.py +0 -0
  144. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/base_content_store.py +0 -0
  145. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/local_content_store.py +0 -0
  146. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/minio_content_store.py +0 -0
  147. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/store/vector_search.py +0 -0
  148. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tasks/workflow_control.py +0 -0
  149. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/teams/__init__.py +0 -0
  150. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/__init__.py +0 -0
  151. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/cli/test_ui.py +0 -0
  152. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_env.py +0 -0
  153. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_log_setup.py +0 -0
  154. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/common/test_lru_cache.py +0 -0
  155. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/integration/__init__.py +0 -0
  156. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
  157. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/model/test_embedding_factory.py +0 -0
  158. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/__init__.py +0 -0
  159. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
  160. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/test_security_profile.py +0 -0
  161. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
  162. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/tasks/__init__.py +0 -0
  163. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/tasks/test_models.py +0 -0
  164. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/test_log_kpi_store.py +0 -0
  165. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
  166. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/users/__init__.py +0 -0
  167. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/users/store/__init__.py +0 -0
  168. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/users/store/base_user_store.py +0 -0
  169. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/users/store/postgres_user_store.py +0 -0
  170. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core/users/user_models.py +0 -0
  171. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core.egg-info/dependency_links.txt +0 -0
  172. {fred_core-3.4.0 → fred_core-3.4.2}/fred_core.egg-info/top_level.txt +0 -0
  173. {fred_core-3.4.0 → fred_core-3.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-core
3
- Version: 3.4.0
3
+ Version: 3.4.2
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,10 @@ 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
95
91
  from fred_core.security.rebac.openfga_engine import OpenFgaRebacEngine
96
92
  from fred_core.security.rebac.rebac_engine import (
97
93
  ORGANIZATION_ID,
94
+ SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS,
98
95
  AgentPermission,
99
96
  DocumentPermission,
100
97
  OrganizationPermission,
@@ -115,6 +112,7 @@ from fred_core.security.structure import (
115
112
  RebacConfiguration,
116
113
  SecurityConfiguration,
117
114
  UserSecurity,
115
+ is_service_agent,
118
116
  )
119
117
  from fred_core.session import SessionSchema
120
118
  from fred_core.session.stores import BaseSessionStore, PostgresSessionStore
@@ -164,15 +162,11 @@ __all__ = [
164
162
  "GcsFilesystem",
165
163
  "FilesystemResourceInfoResult",
166
164
  "FilesystemResourceInfo",
167
- "RBACProvider",
168
- "require_admin",
169
- "require_task_access",
165
+ "is_service_agent",
166
+ "SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS",
170
167
  "Action",
171
168
  "Resource",
172
169
  "AuthorizationError",
173
- "is_authorized",
174
- "authorize_or_raise",
175
- "authorize",
176
170
  "oauth2_scheme",
177
171
  "ClientCredentialsProvider",
178
172
  "BearerAuth",
@@ -38,7 +38,7 @@ from .structures import (
38
38
  StoreConfig,
39
39
  TemporalSchedulerConfig,
40
40
  )
41
- from .team_id import TeamId, personal_team_id
41
+ from .team_id import TeamId, is_personal_team_id, personal_team_id
42
42
  from .utils import raise_internal_error
43
43
 
44
44
  __all__ = [
@@ -58,6 +58,7 @@ __all__ = [
58
58
  "PostgresTableConfig",
59
59
  "StoreConfig",
60
60
  "TeamId",
61
+ "is_personal_team_id",
61
62
  "personal_team_id",
62
63
  "TemporalSchedulerConfig",
63
64
  "ThreadSafeLRUCache",
@@ -22,11 +22,12 @@ from fred_core.security.models import AuthorizationError, Resource
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
24
  _TEAM_PERMISSION_MESSAGES: dict[str, str] = {
25
- "can_update_resources": "You are not allowed to manage resources in this team. Ask a team owner or manager.",
26
- "can_update_agents": "You are not allowed to manage agents in this team. Ask a team owner or manager.",
25
+ "can_update_resources": "You are not allowed to manage resources in this team. Ask a team admin or editor.",
26
+ "can_update_agents": "You are not allowed to manage agents in this team. Ask a team admin or editor.",
27
27
  "can_administer_members": "You are not allowed to manage members in this team.",
28
- "can_administer_managers": "You are not allowed to manage managers in this team.",
29
- "can_administer_owners": "You are not allowed to manage owners in this team.",
28
+ "can_administer_editors": "You are not allowed to manage editors in this team.",
29
+ "can_administer_analysts": "You are not allowed to manage analysts in this team.",
30
+ "can_administer_admins": "You are not allowed to manage admins in this team.",
30
31
  "can_read_members": "You are not allowed to view team members.",
31
32
  "can_update_info": "You are not allowed to update this team.",
32
33
  }
@@ -40,6 +40,11 @@ class TemporalSchedulerConfig(BaseModel):
40
40
  task_queue: str = "default"
41
41
  workflow_id_prefix: str = "task"
42
42
  connect_timeout_seconds: Optional[int] = 5
43
+ rpc_timeout_seconds: Optional[int] = Field(
44
+ default=10,
45
+ description="Deadline applied to individual Temporal RPC calls (start_workflow, describe) "
46
+ "so a stuck Temporal frontend cannot hang the caller indefinitely.",
47
+ )
43
48
  ingestion_workflow_parallelism: int = Field(
44
49
  default=3,
45
50
  ge=1,
@@ -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
@@ -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
 
@@ -17,7 +17,7 @@ from contextlib import asynccontextmanager
17
17
  from fred_core.kpi.base_kpi_writer import BaseKPIWriter
18
18
  from fred_core.kpi.kpi_writer_structures import Dims, KPIActor
19
19
 
20
- PHASE_METRIC_ACTOR = KPIActor(type="system", user_id=None, groups=None)
20
+ PHASE_METRIC_ACTOR = KPIActor(type="system", user_id=None)
21
21
 
22
22
 
23
23
  def record_phase_metric(
@@ -123,7 +123,7 @@ def to_kpi_actor(user: KeycloakUser) -> KPIActor:
123
123
  - Fred’s KPI model is actor-centric (human/agent/system). We always attribute
124
124
  costs and usage to an actor to support per-user chargeback and audits.
125
125
  """
126
- return KPIActor(type="human", user_id=user.uid, groups=user.groups)
126
+ return KPIActor(type="human", user_id=user.uid)
127
127
 
128
128
 
129
129
  # -------------------------
@@ -196,8 +196,6 @@ class KPIWriter(BaseKPIWriter):
196
196
  _dims["actor_type"] = actor.type
197
197
  if actor.user_id:
198
198
  _dims["user_id"] = actor.user_id
199
- if "groups" not in _dims and actor.groups:
200
- _dims["groups"] = ",".join(sorted(actor.groups))
201
199
  merged_dims = _merge_dims(self.defaults, _dims)
202
200
 
203
201
  event = KPIEvent(
@@ -149,7 +149,6 @@ class KPIActor(BaseModel):
149
149
 
150
150
  type: Literal["human", "system"]
151
151
  user_id: Optional[str] = None
152
- groups: Optional[list[str]] = None
153
152
 
154
153
  @field_validator("user_id")
155
154
  @classmethod
@@ -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"},
@@ -99,7 +103,6 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
99
103
  "system_prompt_chars": {"type": "keyword"},
100
104
  "template_id": {"type": "keyword"},
101
105
  "source_runtime_id": {"type": "keyword"},
102
- "groups": {"type": "keyword"},
103
106
  }
104
107
  },
105
108
  "cost": {
@@ -116,6 +119,8 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
116
119
  "bytes_out": {"type": "long"},
117
120
  "chunks": {"type": "integer"},
118
121
  "vectors": {"type": "integer"},
122
+ "input_tokens": {"type": "long"},
123
+ "output_tokens": {"type": "long"},
119
124
  }
120
125
  },
121
126
  "labels": {"type": "keyword"},
@@ -170,6 +175,11 @@ class OpenSearchKPIStore(BaseKPIStore):
170
175
  logger.info("[OPENSEARCH][KPI] created index '%s'", self.index)
171
176
  else:
172
177
  logger.info("[OPENSEARCH][KPI] index '%s' already exists.", self.index)
178
+ # CTRLP-12: session_id was added to KPI_INDEX_MAPPING for RGPD
179
+ # conversation erasure. Existing indices predate it, so add it
180
+ # additively (put_mapping) before validation — otherwise startup
181
+ # fails with "Missing nested field: 'dims.session_id'".
182
+ self._ensure_dim_mapping("session_id", {"type": "keyword"})
173
183
  self._ensure_dim_mapping("service", {"type": "keyword"})
174
184
  self._ensure_dim_mapping("team_id", {"type": "keyword"})
175
185
  self._ensure_dim_mapping("agent_instance_id", {"type": "keyword"})
@@ -181,7 +191,11 @@ class OpenSearchKPIStore(BaseKPIStore):
181
191
  self._ensure_dim_mapping("system_prompt_chars", {"type": "keyword"})
182
192
  self._ensure_dim_mapping("template_id", {"type": "keyword"})
183
193
  self._ensure_dim_mapping("source_runtime_id", {"type": "keyword"})
184
- self._ensure_dim_mapping("groups", {"type": "keyword"})
194
+ # OBSERV-02: quantities.input_tokens/output_tokens were written by
195
+ # agent.turn_completed since Phase 7 but never mapped, so they were
196
+ # unaggregatable. Added additively for existing indices.
197
+ self._ensure_quantities_mapping("input_tokens", {"type": "long"})
198
+ self._ensure_quantities_mapping("output_tokens", {"type": "long"})
185
199
  # Validate existing mapping matches expected mapping
186
200
  validate_index_mapping(self.client, self.index, KPI_INDEX_MAPPING)
187
201
  except OpenSearchException as e:
@@ -209,6 +223,27 @@ class OpenSearchKPIStore(BaseKPIStore):
209
223
  "[OPENSEARCH][KPI] failed to add dims.%s mapping: %s", name, e
210
224
  )
211
225
 
226
+ def _ensure_quantities_mapping(self, name: str, mapping: Dict[str, Any]) -> None:
227
+ try:
228
+ current_mapping_resp = self.client.indices.get_mapping(index=self.index)
229
+ current_mapping = current_mapping_resp.get(self.index, {}).get(
230
+ "mappings", {}
231
+ )
232
+ quantities_props = (
233
+ current_mapping.get("properties", {})
234
+ .get("quantities", {})
235
+ .get("properties", {})
236
+ )
237
+ if name in quantities_props:
238
+ return
239
+ body = {"properties": {"quantities": {"properties": {name: mapping}}}}
240
+ self.client.indices.put_mapping(index=self.index, body=body)
241
+ logger.info("[OPENSEARCH][KPI] added quantities.%s mapping", name)
242
+ except OpenSearchException as e:
243
+ logger.warning(
244
+ "[OPENSEARCH][KPI] failed to add quantities.%s mapping: %s", name, e
245
+ )
246
+
212
247
  # -- writes ----------------------------------------------------------------
213
248
  def index_event(self, event: KPIEvent) -> None:
214
249
  try:
@@ -234,6 +269,57 @@ class OpenSearchKPIStore(BaseKPIStore):
234
269
  logger.error("[OPENSEARCH][KPI] bulk_index failed: %s", e)
235
270
  raise
236
271
 
272
+ # -- erasure (RGPD) --------------------------------------------------------
273
+ def anonymise_for_session(self, session_id: str) -> int:
274
+ """Anonymise — not delete — every KPI row of one conversation (CTRLP-12 A3).
275
+
276
+ RGPD default per RFC §3.3: KPI events are an analytics *aggregate*, not
277
+ conversation content, so erasure severs the link to a person rather than
278
+ dropping the rows. An OpenSearch `update_by_query` (the same primitive the
279
+ vector store uses for `delete_by_query`) nulls the direct identifiers on
280
+ the session's rows: `dims.session_id`, `dims.user_id`, `dims.exchange_id`
281
+ and — for any generic session-scoped analytics row — `dims.scope_id`.
282
+ Aggregate counts are untouched — the rows survive, now anonymous. Returns
283
+ the number of rows anonymised.
284
+
285
+ A session's rows are matched by `dims.session_id == session_id`, the dim
286
+ every session-scoped KPI row actually carries (react/graph `phase_latency`
287
+ and `tool_*` all set `dims.session_id`; see the runtime KPI emitters). A
288
+ pure aggregate with no `session_id` (e.g. `llm.call_latency_ms` emitted
289
+ under a system actor) carries no personal identifier and is correctly out
290
+ of per-conversation scope. Idempotent: once `session_id` is nulled the row
291
+ no longer matches, so a second erase updates zero.
292
+ """
293
+ body: Dict[str, Any] = {
294
+ "query": {"bool": {"filter": [{"term": {"dims.session_id": session_id}}]}},
295
+ "script": {
296
+ "lang": "painless",
297
+ "source": (
298
+ "if (ctx._source.dims != null) "
299
+ "{ for (f in params.fields) { ctx._source.dims.remove(f); } }"
300
+ ),
301
+ "params": {
302
+ "fields": ["session_id", "scope_id", "user_id", "exchange_id"]
303
+ },
304
+ },
305
+ }
306
+ try:
307
+ resp = self.client.update_by_query(
308
+ index=self.index,
309
+ body=body,
310
+ params={"conflicts": "proceed", "refresh": "true"},
311
+ )
312
+ updated = int(resp.get("updated", 0))
313
+ logger.info(
314
+ "[OPENSEARCH][KPI] anonymised %s row(s) for session_id=%s",
315
+ updated,
316
+ session_id,
317
+ )
318
+ return updated
319
+ except OpenSearchException as e:
320
+ logger.error("[OPENSEARCH][KPI] anonymise_for_session failed: %s", e)
321
+ raise
322
+
237
323
  # -- reads -----------------------------------------------------------------
238
324
  def query(self, q: KPIQuery) -> KPIQueryResult:
239
325
  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"
@@ -209,7 +209,6 @@ class MetricsProvider:
209
209
  name: str,
210
210
  *,
211
211
  dims: dict[str, str | None] | None = None,
212
- groups: list[str] | None = None,
213
212
  ) -> Generator[dict[str, str | None], None, None]:
214
213
  """Time a block. Yields a mutable dims dict callers can annotate."""
215
214
  yield dict(dims) if dims else {}
@@ -238,7 +237,6 @@ class LoggingMetricsProvider(MetricsProvider):
238
237
  name: str,
239
238
  *,
240
239
  dims: dict[str, str | None] | None = None,
241
- groups: list[str] | None = None,
242
240
  ) -> Generator[dict[str, str | None], None, None]:
243
241
  import time as _time
244
242
 
@@ -258,7 +256,6 @@ class LoggingMetricsProvider(MetricsProvider):
258
256
  "name": name,
259
257
  "dims": d,
260
258
  "duration_ms": round((_time.perf_counter() - start) * 1000, 2),
261
- "groups": groups,
262
259
  }
263
260
  },
264
261
  )
@@ -305,7 +302,6 @@ class InMemoryMetricsProvider(MetricsProvider):
305
302
  name: str,
306
303
  *,
307
304
  dims: dict[str, str | None] | None = None,
308
- groups: list[str] | None = None,
309
305
  ) -> Generator[dict[str, str | None], None, None]:
310
306
  import time as _time
311
307
 
@@ -54,10 +54,27 @@ class TemporalClientProvider:
54
54
  self._config.host,
55
55
  self._config.namespace,
56
56
  )
57
- # temporalio Client.connect is async
58
- self._client = await Client.connect(
57
+ # temporalio Client.connect is async and exposes no per-call rpc_timeout of
58
+ # its own — bound it ourselves so an unreachable/slow Temporal frontend
59
+ # cannot hang every caller of get_client() indefinitely.
60
+ connect_call = Client.connect(
59
61
  self._config.host,
60
62
  namespace=self._config.namespace,
61
63
  data_converter=pydantic_data_converter,
62
64
  )
65
+ timeout = self._config.connect_timeout_seconds
66
+ try:
67
+ self._client = (
68
+ await asyncio.wait_for(connect_call, timeout=timeout)
69
+ if timeout
70
+ else await connect_call
71
+ )
72
+ except TimeoutError:
73
+ logger.error(
74
+ "[TEMPORAL] Connect to host=%s namespace=%s timed out after %ss",
75
+ self._config.host,
76
+ self._config.namespace,
77
+ timeout,
78
+ )
79
+ raise
63
80
  return self._client
@@ -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,