fred-core 3.4.1__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 (170) hide show
  1. {fred_core-3.4.1 → fred_core-3.4.2}/PKG-INFO +1 -1
  2. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/__init__.py +0 -2
  3. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/__init__.py +2 -1
  4. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/fastapi_handlers.py +5 -4
  5. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/structures.py +5 -0
  6. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_phase_metric.py +1 -1
  7. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_writer.py +1 -3
  8. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_writer_structures.py +0 -1
  9. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/opensearch_kpi_store.py +28 -2
  10. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/portable/observability.py +0 -4
  11. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/temporal_client_provider.py +19 -2
  12. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/models.py +0 -1
  13. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/oidc.py +0 -3
  14. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/noop_engine.py +11 -0
  15. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/openfga_engine.py +71 -6
  16. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/rebac_engine.py +92 -125
  17. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/rebac_factory.py +2 -2
  18. fred_core-3.4.2/fred_core/security/rebac/schema.fga +215 -0
  19. fred_core-3.4.2/fred_core/security/rebac/schema.fga.json +1 -0
  20. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/structure.py +16 -1
  21. fred_core-3.4.2/fred_core/teams/metadata_store.py +335 -0
  22. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/teams/team_metatada_models.py +8 -0
  23. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_fastapi_handlers.py +1 -1
  24. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/integration/test_rebac.py +514 -141
  25. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/portable/test_observability.py +1 -3
  26. fred_core-3.4.2/fred_core/tests/scheduler/test_temporal_client_provider.py +84 -0
  27. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_oidc_strict.py +27 -0
  28. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_engine_team_helpers.py +99 -10
  29. fred_core-3.4.2/fred_core/tests/security/test_rebac_schema_authz05.py +186 -0
  30. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_service_agent.py +1 -3
  31. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_authz.py +1 -1
  32. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/PKG-INFO +1 -1
  33. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/SOURCES.txt +2 -2
  34. {fred_core-3.4.1 → fred_core-3.4.2}/pyproject.toml +1 -1
  35. fred_core-3.4.1/fred_core/security/permission_catalog.py +0 -90
  36. fred_core-3.4.1/fred_core/security/rebac/schema.fga +0 -168
  37. fred_core-3.4.1/fred_core/security/rebac/schema.fga.json +0 -1
  38. fred_core-3.4.1/fred_core/teams/metadata_store.py +0 -206
  39. fred_core-3.4.1/fred_core/tests/security/test_authorization.py +0 -54
  40. {fred_core-3.4.1 → fred_core-3.4.2}/README.md +0 -0
  41. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/__init__.py +0 -0
  42. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/auth.py +0 -0
  43. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/ui.py +0 -0
  44. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/config_files.py +0 -0
  45. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/config_loader.py +0 -0
  46. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/env.py +0 -0
  47. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/lru_cache.py +0 -0
  48. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/team_id.py +0 -0
  49. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/utils.py +0 -0
  50. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/__init__.py +0 -0
  51. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_models.py +0 -0
  52. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_store.py +0 -0
  53. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_structures.py +0 -0
  54. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/postgres_document_store.py +0 -0
  55. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/tag_models.py +0 -0
  56. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/gcs_filesystem.py +0 -0
  57. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/local_filesystem.py +0 -0
  58. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/minio_filesystem.py +0 -0
  59. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/structures.py +0 -0
  60. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/__init__.py +0 -0
  61. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/base_history_store.py +0 -0
  62. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/history_models.py +0 -0
  63. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/history_schema.py +0 -0
  64. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/postgres_history_store.py +0 -0
  65. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/__init__.py +0 -0
  66. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/base_kpi_store.py +0 -0
  67. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/base_kpi_writer.py +0 -0
  68. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/http_middleware.py +0 -0
  69. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_factory.py +0 -0
  70. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_process.py +0 -0
  71. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_reader_structures.py +0 -0
  72. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/log_kpi_store.py +0 -0
  73. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/noop_kpi_writer.py +0 -0
  74. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/prometheus_kpi_store.py +0 -0
  75. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/__init__.py +0 -0
  76. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/base_log_store.py +0 -0
  77. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/log_setup.py +0 -0
  78. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/log_structures.py +0 -0
  79. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/memory_log_store.py +0 -0
  80. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/null_log_store.py +0 -0
  81. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/opensearch_log_store.py +0 -0
  82. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/factory.py +0 -0
  83. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/http_clients.py +0 -0
  84. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/models.py +0 -0
  85. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/models/__init__.py +0 -0
  86. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/models/base.py +0 -0
  87. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/portable/__init__.py +0 -0
  88. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/py.typed +0 -0
  89. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/__init__.py +0 -0
  90. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/backend.py +0 -0
  91. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/scheduler_structures.py +0 -0
  92. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/__init__.py +0 -0
  93. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/authorization.py +0 -0
  94. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/backend_to_backend_auth.py +0 -0
  95. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/keycloak/__init__.py +0 -0
  96. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/keycloak/keycloack_admin_client.py +0 -0
  97. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/outbound.py +0 -0
  98. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/openfga_schema.py +0 -0
  99. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/whitelist_access_control/access_control.py +0 -0
  100. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/__init__.py +0 -0
  101. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/session_schema.py +0 -0
  102. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/__init__.py +0 -0
  103. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/base_session_store.py +0 -0
  104. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/postgres_session_store.py +0 -0
  105. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/session_models.py +0 -0
  106. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/__init__.py +0 -0
  107. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/alembic_env.py +0 -0
  108. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/async_session.py +0 -0
  109. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/base_sql.py +0 -0
  110. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/mixin.py +0 -0
  111. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/__init__.py +0 -0
  112. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/base_content_store.py +0 -0
  113. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/local_content_store.py +0 -0
  114. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/minio_content_store.py +0 -0
  115. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/opensearch_mapping_validator.py +0 -0
  116. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/vector_search.py +0 -0
  117. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/__init__.py +0 -0
  118. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/authz.py +0 -0
  119. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/bus.py +0 -0
  120. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/models.py +0 -0
  121. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/orm_models.py +0 -0
  122. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/service.py +0 -0
  123. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/sse.py +0 -0
  124. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/store.py +0 -0
  125. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/workflow_control.py +0 -0
  126. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/teams/__init__.py +0 -0
  127. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/__init__.py +0 -0
  128. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/cli/test_auth.py +0 -0
  129. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/cli/test_ui.py +0 -0
  130. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_config_loader.py +0 -0
  131. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_env.py +0 -0
  132. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_log_setup.py +0 -0
  133. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_lru_cache.py +0 -0
  134. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_structures.py +0 -0
  135. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/filesystem/test_gcs_filesystem.py +0 -0
  136. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/filesystem/test_local_filesystem.py +0 -0
  137. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/integration/__init__.py +0 -0
  138. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_http_middleware.py +0 -0
  139. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
  140. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +0 -0
  141. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +0 -0
  142. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/logs/test_memory_log_store.py +0 -0
  143. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_anthropic_factory.py +0 -0
  144. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_embedding_factory.py +0 -0
  145. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_http_clients.py +0 -0
  146. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -0
  147. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/scheduler/test_backend.py +0 -0
  148. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/__init__.py +0 -0
  149. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
  150. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_security_profile.py +0 -0
  151. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
  152. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -0
  153. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/store/test_local_content_store.py +0 -0
  154. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/__init__.py +0 -0
  155. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_bus.py +0 -0
  156. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_models.py +0 -0
  157. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_reconcile.py +0 -0
  158. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_scheduling.py +0 -0
  159. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_sse.py +0 -0
  160. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/test_log_kpi_store.py +0 -0
  161. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
  162. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/__init__.py +0 -0
  163. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/__init__.py +0 -0
  164. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/base_user_store.py +0 -0
  165. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/postgres_user_store.py +0 -0
  166. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/user_models.py +0 -0
  167. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/dependency_links.txt +0 -0
  168. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/requires.txt +0 -0
  169. {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/top_level.txt +0 -0
  170. {fred_core-3.4.1 → 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.1
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
@@ -88,7 +88,6 @@ from fred_core.security.oidc import (
88
88
  split_realm_url,
89
89
  )
90
90
  from fred_core.security.outbound import BearerAuth, ClientCredentialsProvider
91
- from fred_core.security.permission_catalog import list_display_permissions
92
91
  from fred_core.security.rebac.openfga_engine import OpenFgaRebacEngine
93
92
  from fred_core.security.rebac.rebac_engine import (
94
93
  ORGANIZATION_ID,
@@ -165,7 +164,6 @@ __all__ = [
165
164
  "FilesystemResourceInfo",
166
165
  "is_service_agent",
167
166
  "SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS",
168
- "list_display_permissions",
169
167
  "Action",
170
168
  "Resource",
171
169
  "AuthorizationError",
@@ -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,
@@ -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
@@ -103,7 +103,6 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
103
103
  "system_prompt_chars": {"type": "keyword"},
104
104
  "template_id": {"type": "keyword"},
105
105
  "source_runtime_id": {"type": "keyword"},
106
- "groups": {"type": "keyword"},
107
106
  }
108
107
  },
109
108
  "cost": {
@@ -120,6 +119,8 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
120
119
  "bytes_out": {"type": "long"},
121
120
  "chunks": {"type": "integer"},
122
121
  "vectors": {"type": "integer"},
122
+ "input_tokens": {"type": "long"},
123
+ "output_tokens": {"type": "long"},
123
124
  }
124
125
  },
125
126
  "labels": {"type": "keyword"},
@@ -190,7 +191,11 @@ class OpenSearchKPIStore(BaseKPIStore):
190
191
  self._ensure_dim_mapping("system_prompt_chars", {"type": "keyword"})
191
192
  self._ensure_dim_mapping("template_id", {"type": "keyword"})
192
193
  self._ensure_dim_mapping("source_runtime_id", {"type": "keyword"})
193
- 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"})
194
199
  # Validate existing mapping matches expected mapping
195
200
  validate_index_mapping(self.client, self.index, KPI_INDEX_MAPPING)
196
201
  except OpenSearchException as e:
@@ -218,6 +223,27 @@ class OpenSearchKPIStore(BaseKPIStore):
218
223
  "[OPENSEARCH][KPI] failed to add dims.%s mapping: %s", name, e
219
224
  )
220
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
+
221
247
  # -- writes ----------------------------------------------------------------
222
248
  def index_event(self, event: KPIEvent) -> None:
223
249
  try:
@@ -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
@@ -41,7 +41,6 @@ class Resource(str, Enum):
41
41
  TABLES_DATABASES = "tables_database"
42
42
  KPIS = "kpi"
43
43
  OPENSEARCH = "opensearch"
44
- NEO4J = "neo4j"
45
44
  LOGS = "logs"
46
45
  FILES = "files"
47
46
 
@@ -308,7 +308,6 @@ def decode_jwt(token: str) -> KeycloakUser:
308
308
  username=username,
309
309
  roles=["admin"],
310
310
  email=f"{username}@localhost",
311
- groups=["admins"],
312
311
  )
313
312
 
314
313
  cached_user = _get_cached_user(token)
@@ -434,7 +433,6 @@ def decode_jwt(token: str) -> KeycloakUser:
434
433
  username=payload.get("preferred_username", ""),
435
434
  roles=client_roles,
436
435
  email=payload.get("email"),
437
- groups=payload.get("groups", []),
438
436
  )
439
437
  logger.debug("KeycloakUser built: %s", user)
440
438
  _cache_user(token, payload, user)
@@ -498,7 +496,6 @@ async def get_current_user_without_gcu(
498
496
  username="admin",
499
497
  roles=["admin"],
500
498
  email="admin@mail.com",
501
- groups=["admins"],
502
499
  )
503
500
 
504
501
  if not token:
@@ -83,6 +83,17 @@ class NoopRebacEngine(RebacEngine):
83
83
  ) -> list[RebacReference] | RebacDisabledResult:
84
84
  return RebacDisabledResult()
85
85
 
86
+ async def has_direct_relation(
87
+ self,
88
+ subject: RebacReference,
89
+ relation: RelationType,
90
+ resource: RebacReference,
91
+ *,
92
+ consistency_token: str | None = None,
93
+ ) -> bool:
94
+ # Nothing is ever persisted under the no-op engine.
95
+ return False
96
+
86
97
  async def has_permission(
87
98
  self,
88
99
  subject: RebacReference,
@@ -28,13 +28,14 @@ from fred_core.security.rebac.openfga_schema import (
28
28
  DEFAULT_SCHEMA,
29
29
  )
30
30
  from fred_core.security.rebac.rebac_engine import (
31
+ ORGANIZATION_ID,
31
32
  RebacEngine,
32
33
  RebacPermission,
33
34
  RebacReference,
34
35
  Relation,
35
36
  RelationType,
36
37
  )
37
- from fred_core.security.structure import M2MSecurity, OpenFgaRebacConfig
38
+ from fred_core.security.structure import OpenFgaRebacConfig
38
39
  from openfga_sdk.client.client import OpenFgaClient
39
40
  from openfga_sdk.client.configuration import ClientConfiguration
40
41
  from openfga_sdk.client.models.check_request import ClientCheckRequest
@@ -72,13 +73,10 @@ class OpenFgaRebacEngine(RebacEngine):
72
73
  def __init__(
73
74
  self,
74
75
  config: OpenFgaRebacConfig,
75
- m2m_security: M2MSecurity,
76
76
  *,
77
77
  token: str | None = None,
78
78
  schema: str = DEFAULT_SCHEMA,
79
79
  ) -> None:
80
- super().__init__(m2m_security)
81
-
82
80
  resolved_token = token or os.getenv(config.token_env_var)
83
81
  if not resolved_token:
84
82
  raise ValueError(
@@ -193,8 +191,12 @@ class OpenFgaRebacEngine(RebacEngine):
193
191
  subject_type: Resource | None = None,
194
192
  consistency_token: str | None = None,
195
193
  ) -> list[Relation]:
196
- # Only used to sync Keycloakc groups with the rebac engine. Not needed
197
- # with OpenFGA as we handle this with contextual tuples.
194
+ # No current caller needs a bulk tuple read: every authorization
195
+ # decision goes through `has_permission`, `lookup_resources`, or
196
+ # `lookup_subjects` instead, each backed by a persisted OpenFGA tuple
197
+ # (never Keycloak-derived). Implemented by 2 other engines and
198
+ # exercised elsewhere; this OpenFGA-backed stub stays a stub until a
199
+ # real caller needs it.
198
200
  raise NotImplementedError(
199
201
  "OpenFGA relation listing is not implemented as it is not needed"
200
202
  )
@@ -256,6 +258,30 @@ class OpenFgaRebacEngine(RebacEngine):
256
258
  for user in response.users
257
259
  ]
258
260
 
261
+ async def has_direct_relation(
262
+ self,
263
+ subject: RebacReference,
264
+ relation: RelationType,
265
+ resource: RebacReference,
266
+ *,
267
+ consistency_token: str | None = None,
268
+ ) -> bool:
269
+ # A fully-specified tuple key (user + relation + object) queried via
270
+ # the raw `Read` API returns only literally-persisted tuples — unlike
271
+ # `Check`/`ListUsers`, it does not expand userset rewrites (e.g.
272
+ # schema.fga's `team_member: [user] or team_admin or team_editor or
273
+ # team_analyst`). This is the same primitive already used by
274
+ # `delete_all_relations_of_reference` for a bulk tuple read.
275
+ client = await self.get_client()
276
+ body = ReadRequestTupleKey(
277
+ user=OpenFgaRebacEngine._reference_to_openfga_id(subject),
278
+ relation=relation.value,
279
+ object=OpenFgaRebacEngine._reference_to_openfga_id(resource),
280
+ )
281
+ options = self._build_options(consistency=consistency_token)
282
+ response = await client.read(body, options)
283
+ return len(response.tuples) > 0
284
+
259
285
  async def has_permission(
260
286
  self,
261
287
  subject: RebacReference,
@@ -375,12 +401,51 @@ class OpenFgaRebacEngine(RebacEngine):
375
401
  if self._config.sync_schema_on_init:
376
402
  await self.sync_schema(client)
377
403
 
404
+ # AUTHZ-05 bootstrap (RFC §24.3): config-seeded platform_admin/platform_observer.
405
+ await self._bootstrap_platform_roles(client)
406
+
378
407
  logger.info(
379
408
  "[REBAC] OpenFGA engine initialized in %dms",
380
409
  int((time.monotonic() - started) * 1000),
381
410
  )
382
411
  return client
383
412
 
413
+ async def _bootstrap_platform_roles(self, client: OpenFgaClient) -> None:
414
+ """Write config-seeded platform_admin/platform_observer tuples once.
415
+
416
+ Called with the client already resolved (not `self.get_client()`) because
417
+ this runs from inside `_initialize_client_and_store`, which itself holds
418
+ the client-initialization lock — going through `add_relation` here would
419
+ deadlock on that lock. Writes are idempotent (`_build_options` sets
420
+ `on_duplicate_writes=IGNORE`) and never remove an existing grant.
421
+ """
422
+ relations = [
423
+ Relation(
424
+ subject=RebacReference(Resource.USER, sub),
425
+ relation=RelationType.PLATFORM_ADMIN,
426
+ resource=RebacReference(Resource.ORGANIZATION, ORGANIZATION_ID),
427
+ )
428
+ for sub in self._config.platform_admin_subjects
429
+ ] + [
430
+ Relation(
431
+ subject=RebacReference(Resource.USER, sub),
432
+ relation=RelationType.PLATFORM_OBSERVER,
433
+ resource=RebacReference(Resource.ORGANIZATION, ORGANIZATION_ID),
434
+ )
435
+ for sub in self._config.platform_observer_subjects
436
+ ]
437
+ if not relations:
438
+ return
439
+
440
+ logger.info(
441
+ "[REBAC] Bootstrapping %d config-seeded platform-role tuple(s)",
442
+ len(relations),
443
+ )
444
+ body = ClientWriteRequest(
445
+ writes=[OpenFgaRebacEngine._relation_to_tuple(r) for r in relations]
446
+ )
447
+ _ = await client.write(body, self._build_options())
448
+
384
449
  async def get_client(self) -> OpenFgaClient:
385
450
  """Lazily initialize and cache an OpenFGA client with store ID."""
386
451
  if self._cached_client is None: