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.
- {fred_core-3.4.1 → fred_core-3.4.2}/PKG-INFO +1 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/__init__.py +0 -2
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/__init__.py +2 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/fastapi_handlers.py +5 -4
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/structures.py +5 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_phase_metric.py +1 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_writer.py +1 -3
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_writer_structures.py +0 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/opensearch_kpi_store.py +28 -2
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/portable/observability.py +0 -4
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/temporal_client_provider.py +19 -2
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/models.py +0 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/oidc.py +0 -3
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/noop_engine.py +11 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/openfga_engine.py +71 -6
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/rebac_engine.py +92 -125
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/rebac_factory.py +2 -2
- fred_core-3.4.2/fred_core/security/rebac/schema.fga +215 -0
- fred_core-3.4.2/fred_core/security/rebac/schema.fga.json +1 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/structure.py +16 -1
- fred_core-3.4.2/fred_core/teams/metadata_store.py +335 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/teams/team_metatada_models.py +8 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_fastapi_handlers.py +1 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/integration/test_rebac.py +514 -141
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/portable/test_observability.py +1 -3
- fred_core-3.4.2/fred_core/tests/scheduler/test_temporal_client_provider.py +84 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_oidc_strict.py +27 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_engine_team_helpers.py +99 -10
- fred_core-3.4.2/fred_core/tests/security/test_rebac_schema_authz05.py +186 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_service_agent.py +1 -3
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_authz.py +1 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/PKG-INFO +1 -1
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/SOURCES.txt +2 -2
- {fred_core-3.4.1 → fred_core-3.4.2}/pyproject.toml +1 -1
- fred_core-3.4.1/fred_core/security/permission_catalog.py +0 -90
- fred_core-3.4.1/fred_core/security/rebac/schema.fga +0 -168
- fred_core-3.4.1/fred_core/security/rebac/schema.fga.json +0 -1
- fred_core-3.4.1/fred_core/teams/metadata_store.py +0 -206
- fred_core-3.4.1/fred_core/tests/security/test_authorization.py +0 -54
- {fred_core-3.4.1 → fred_core-3.4.2}/README.md +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/auth.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/cli/ui.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/config_files.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/config_loader.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/env.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/lru_cache.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/team_id.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/common/utils.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/document_structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/postgres_document_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/documents/tag_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/gcs_filesystem.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/local_filesystem.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/minio_filesystem.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/filesystem/structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/base_history_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/history_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/history_schema.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/history/postgres_history_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/base_kpi_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/base_kpi_writer.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/http_middleware.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_factory.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_process.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/kpi_reader_structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/log_kpi_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/noop_kpi_writer.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/kpi/prometheus_kpi_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/base_log_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/log_setup.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/log_structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/memory_log_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/null_log_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/logs/opensearch_log_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/factory.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/http_clients.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/model/models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/models/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/models/base.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/portable/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/py.typed +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/backend.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/scheduler/scheduler_structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/authorization.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/backend_to_backend_auth.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/keycloak/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/keycloak/keycloack_admin_client.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/outbound.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/rebac/openfga_schema.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/security/whitelist_access_control/access_control.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/session_schema.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/base_session_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/postgres_session_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/session/stores/session_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/alembic_env.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/async_session.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/base_sql.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/sql/mixin.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/base_content_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/local_content_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/minio_content_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/opensearch_mapping_validator.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/store/vector_search.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/authz.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/bus.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/orm_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/service.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/sse.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tasks/workflow_control.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/teams/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/cli/test_auth.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/cli/test_ui.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_config_loader.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_env.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_log_setup.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_lru_cache.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/common/test_structures.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/filesystem/test_gcs_filesystem.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/filesystem/test_local_filesystem.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/integration/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_http_middleware.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/logs/test_memory_log_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_anthropic_factory.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_embedding_factory.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_http_clients.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/scheduler/test_backend.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_security_profile.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/store/test_local_content_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_bus.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_reconcile.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_scheduling.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/tasks/test_sse.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/test_log_kpi_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/__init__.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/base_user_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/store/postgres_user_store.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core/users/user_models.py +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/dependency_links.txt +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/requires.txt +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/fred_core.egg-info/top_level.txt +0 -0
- {fred_core-3.4.1 → fred_core-3.4.2}/setup.cfg +0 -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
|
|
26
|
-
"can_update_agents": "You are not allowed to manage agents in this team. Ask a team
|
|
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
|
-
"
|
|
29
|
-
"
|
|
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
|
|
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
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
#
|
|
197
|
-
#
|
|
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:
|