fred-core 3.2.0__tar.gz → 3.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fred_core-3.2.0 → fred_core-3.4.1}/PKG-INFO +4 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/__init__.py +11 -15
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/config_loader.py +41 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/structures.py +20 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/__init__.py +2 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_store.py +0 -10
- fred_core-3.4.1/fred_core/documents/tag_models.py +26 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/gcs_filesystem.py +2 -3
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/local_filesystem.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/minio_filesystem.py +2 -3
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/base_history_store.py +22 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/postgres_history_store.py +15 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/opensearch_kpi_store.py +60 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/log_setup.py +1 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/factory.py +59 -4
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/http_clients.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/model/models.py +1 -0
- fred_core-3.4.1/fred_core/security/authorization.py +43 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/keycloak/keycloack_admin_client.py +0 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/models.py +0 -17
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/oidc.py +69 -13
- fred_core-3.4.1/fred_core/security/permission_catalog.py +90 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/openfga_engine.py +12 -13
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/rebac_engine.py +38 -3
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/schema.fga +22 -0
- fred_core-3.4.1/fred_core/security/rebac/schema.fga.json +1 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/structure.py +27 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/opensearch_mapping_validator.py +0 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/__init__.py +14 -0
- fred_core-3.4.1/fred_core/tasks/authz.py +143 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/bus.py +80 -22
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/models.py +74 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/orm_models.py +7 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/service.py +64 -11
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/sse.py +40 -16
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/store.py +17 -3
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/metadata_store.py +17 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/team_metatada_models.py +8 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/cli/test_auth.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_config_loader.py +28 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_fastapi_handlers.py +0 -1
- fred_core-3.4.1/fred_core/tests/common/test_structures.py +46 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/filesystem/test_gcs_filesystem.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/filesystem/test_local_filesystem.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/kpi/test_http_middleware.py +0 -1
- fred_core-3.4.1/fred_core/tests/kpi/test_opensearch_kpi_store_anonymise.py +199 -0
- fred_core-3.4.1/fred_core/tests/kpi/test_opensearch_kpi_store_ensure_ready.py +104 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/logs/test_memory_log_store.py +0 -1
- fred_core-3.4.1/fred_core/tests/model/test_anthropic_factory.py +177 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_http_clients.py +1 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_vertex_model_garden_auth.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/portable/test_observability.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/scheduler/test_backend.py +0 -1
- fred_core-3.4.1/fred_core/tests/security/test_authorization.py +54 -0
- fred_core-3.4.1/fred_core/tests/security/test_oidc_strict.py +101 -0
- fred_core-3.4.1/fred_core/tests/security/test_security_profile.py +98 -0
- fred_core-3.4.1/fred_core/tests/security/test_service_agent.py +59 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/session/test_postgres_json_session_store_sqlite.py +0 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/store/test_local_content_store.py +0 -1
- fred_core-3.4.1/fred_core/tests/tasks/test_authz.py +307 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_bus.py +32 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_reconcile.py +36 -0
- fred_core-3.4.1/fred_core/tests/tasks/test_scheduling.py +185 -0
- fred_core-3.4.1/fred_core/tests/tasks/test_sse.py +136 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/PKG-INFO +4 -2
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/SOURCES.txt +13 -3
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/requires.txt +3 -1
- {fred_core-3.2.0 → fred_core-3.4.1}/pyproject.toml +7 -2
- fred_core-3.2.0/fred_core/security/authorization.py +0 -82
- fred_core-3.2.0/fred_core/security/authorization_decorator.py +0 -66
- fred_core-3.2.0/fred_core/security/rbac.py +0 -134
- fred_core-3.2.0/fred_core/security/rebac/schema.fga.json +0 -1
- fred_core-3.2.0/fred_core/tests/security/test_authorization.py +0 -176
- fred_core-3.2.0/fred_core/tests/security/test_authorization_decorator.py +0 -136
- {fred_core-3.2.0 → fred_core-3.4.1}/README.md +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/auth.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/cli/ui.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/config_files.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/env.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/fastapi_handlers.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/lru_cache.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/team_id.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/common/utils.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_models.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/document_structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/documents/postgres_document_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/filesystem/structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/history_models.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/history/history_schema.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/base_kpi_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/base_kpi_writer.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/http_middleware.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_factory.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_phase_metric.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_process.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_reader_structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_writer.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/kpi_writer_structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/log_kpi_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/noop_kpi_writer.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/kpi/prometheus_kpi_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/base_log_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/log_structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/memory_log_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/null_log_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/logs/opensearch_log_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/models/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/models/base.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/portable/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/portable/observability.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/py.typed +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/backend.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/scheduler_structures.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/scheduler/temporal_client_provider.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/backend_to_backend_auth.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/keycloak/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/outbound.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/noop_engine.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/openfga_schema.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/rebac/rebac_factory.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/security/whitelist_access_control/access_control.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/session_schema.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/base_session_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/postgres_session_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/session/stores/session_models.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/alembic_env.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/async_session.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/base_sql.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/sql/mixin.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/base_content_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/local_content_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/minio_content_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/store/vector_search.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tasks/workflow_control.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/teams/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/cli/test_ui.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_env.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_log_setup.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/common/test_lru_cache.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/integration/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/integration/test_rebac.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/kpi/test_noop_kpi_writer.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/model/test_embedding_factory.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_rebac_config_defaults.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_rebac_engine_team_helpers.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/security/test_whitelist_access_control.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/tasks/test_models.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/test_log_kpi_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/tests/test_prometheus_kpi_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/__init__.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/base_user_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/store/postgres_user_store.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core/users/user_models.py +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/dependency_links.txt +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/fred_core.egg-info/top_level.txt +0 -0
- {fred_core-3.2.0 → fred_core-3.4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.1
|
|
4
4
|
Summary: Core shared utilities for Fred backends (config, storage, security, and runtime helpers).
|
|
5
5
|
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,7 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: fastapi==0.
|
|
15
|
+
Requires-Dist: fastapi==0.139.0
|
|
16
|
+
Requires-Dist: starlette>=1.3.1
|
|
16
17
|
Requires-Dist: prometheus-client>=0.20.5
|
|
17
18
|
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
18
19
|
Requires-Dist: psycopg[binary]>=3.1.0
|
|
@@ -28,6 +29,7 @@ Requires-Dist: langchain-google-genai>=4.2.1
|
|
|
28
29
|
Requires-Dist: langchain-ollama>=1.0.1
|
|
29
30
|
Requires-Dist: langchain-mistralai>=1.1.2
|
|
30
31
|
Requires-Dist: langchain-google-vertexai>=3.2.2
|
|
32
|
+
Requires-Dist: langchain-anthropic>=0.3.0
|
|
31
33
|
Requires-Dist: langchain-openai>=1.1.14
|
|
32
34
|
Requires-Dist: azure-identity>=1.25.0
|
|
33
35
|
Requires-Dist: authzed>=1.22.1
|
|
@@ -61,15 +61,7 @@ from fred_core.model.models import ModelProvider
|
|
|
61
61
|
from fred_core.security.authorization import (
|
|
62
62
|
NO_AUTHZ_CHECK_USER,
|
|
63
63
|
TODO_PASS_REAL_USER,
|
|
64
|
-
Action,
|
|
65
|
-
AuthorizationError,
|
|
66
|
-
Resource,
|
|
67
|
-
authorize_or_raise,
|
|
68
|
-
is_authorized,
|
|
69
|
-
require_admin,
|
|
70
|
-
require_task_access,
|
|
71
64
|
)
|
|
72
|
-
from fred_core.security.authorization_decorator import authorize
|
|
73
65
|
from fred_core.security.backend_to_backend_auth import (
|
|
74
66
|
M2MAuthConfig,
|
|
75
67
|
M2MBearerAuth,
|
|
@@ -80,6 +72,11 @@ from fred_core.security.keycloak.keycloack_admin_client import (
|
|
|
80
72
|
KeycloackDisabled,
|
|
81
73
|
create_keycloak_admin,
|
|
82
74
|
)
|
|
75
|
+
from fred_core.security.models import (
|
|
76
|
+
Action,
|
|
77
|
+
AuthorizationError,
|
|
78
|
+
Resource,
|
|
79
|
+
)
|
|
83
80
|
from fred_core.security.oidc import (
|
|
84
81
|
decode_jwt,
|
|
85
82
|
get_current_user,
|
|
@@ -91,10 +88,11 @@ from fred_core.security.oidc import (
|
|
|
91
88
|
split_realm_url,
|
|
92
89
|
)
|
|
93
90
|
from fred_core.security.outbound import BearerAuth, ClientCredentialsProvider
|
|
94
|
-
from fred_core.security.
|
|
91
|
+
from fred_core.security.permission_catalog import list_display_permissions
|
|
95
92
|
from fred_core.security.rebac.openfga_engine import OpenFgaRebacEngine
|
|
96
93
|
from fred_core.security.rebac.rebac_engine import (
|
|
97
94
|
ORGANIZATION_ID,
|
|
95
|
+
SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS,
|
|
98
96
|
AgentPermission,
|
|
99
97
|
DocumentPermission,
|
|
100
98
|
OrganizationPermission,
|
|
@@ -115,6 +113,7 @@ from fred_core.security.structure import (
|
|
|
115
113
|
RebacConfiguration,
|
|
116
114
|
SecurityConfiguration,
|
|
117
115
|
UserSecurity,
|
|
116
|
+
is_service_agent,
|
|
118
117
|
)
|
|
119
118
|
from fred_core.session import SessionSchema
|
|
120
119
|
from fred_core.session.stores import BaseSessionStore, PostgresSessionStore
|
|
@@ -164,15 +163,12 @@ __all__ = [
|
|
|
164
163
|
"GcsFilesystem",
|
|
165
164
|
"FilesystemResourceInfoResult",
|
|
166
165
|
"FilesystemResourceInfo",
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
166
|
+
"is_service_agent",
|
|
167
|
+
"SERVICE_AGENT_ALLOWED_TEAM_PERMISSIONS",
|
|
168
|
+
"list_display_permissions",
|
|
170
169
|
"Action",
|
|
171
170
|
"Resource",
|
|
172
171
|
"AuthorizationError",
|
|
173
|
-
"is_authorized",
|
|
174
|
-
"authorize_or_raise",
|
|
175
|
-
"authorize",
|
|
176
172
|
"oauth2_scheme",
|
|
177
173
|
"ClientCredentialsProvider",
|
|
178
174
|
"BearerAuth",
|
|
@@ -14,15 +14,49 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import sys
|
|
17
18
|
from typing import Callable, TypeVar
|
|
18
19
|
|
|
19
20
|
import yaml
|
|
21
|
+
from pydantic import ValidationError
|
|
20
22
|
|
|
21
23
|
from .config_files import ConfigFiles
|
|
22
24
|
|
|
23
25
|
TConfig = TypeVar("TConfig")
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _render_config_error_banner(config_file: str, error: Exception) -> None:
|
|
29
|
+
"""Print a loud, unmissable configuration-error banner to stderr.
|
|
30
|
+
|
|
31
|
+
A misconfigured service must not start silently and fail later with an
|
|
32
|
+
opaque error inside a request handler. We surface the root cause in red at
|
|
33
|
+
startup. Colours are emitted only on a TTY so log files stay clean.
|
|
34
|
+
"""
|
|
35
|
+
use_colour = sys.stderr.isatty()
|
|
36
|
+
red = "\033[1;31m" if use_colour else ""
|
|
37
|
+
reset = "\033[0m" if use_colour else ""
|
|
38
|
+
bar = "=" * 78
|
|
39
|
+
|
|
40
|
+
if isinstance(error, ValidationError):
|
|
41
|
+
details = "\n".join(
|
|
42
|
+
f" - {' -> '.join(str(p) for p in err['loc']) or '(root)'}: {err['msg']}"
|
|
43
|
+
for err in error.errors()
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
details = f" - {error}"
|
|
47
|
+
|
|
48
|
+
print(
|
|
49
|
+
f"\n{red}{bar}\n"
|
|
50
|
+
f" CONFIGURATION ERROR — refusing to start\n"
|
|
51
|
+
f" file: {config_file}\n"
|
|
52
|
+
f"{bar}{reset}\n"
|
|
53
|
+
f"{details}\n"
|
|
54
|
+
f"{red}{bar}{reset}\n",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
flush=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
26
60
|
def parse_yaml_mapping_file(config_file: str) -> dict:
|
|
27
61
|
"""Load a YAML file and ensure it is a non-empty mapping."""
|
|
28
62
|
with open(config_file, encoding="utf-8") as file:
|
|
@@ -42,7 +76,13 @@ def load_configuration_with_config_files(
|
|
|
42
76
|
"""Load env + config path using ConfigFiles and parse via callback."""
|
|
43
77
|
config_files.load_environment(dotenv_path)
|
|
44
78
|
config_file = config_files.resolve_config_file_path()
|
|
45
|
-
|
|
79
|
+
try:
|
|
80
|
+
configuration = parser(config_file)
|
|
81
|
+
except (ValidationError, ValueError) as exc:
|
|
82
|
+
# Render the root cause in red and stop, rather than letting an opaque
|
|
83
|
+
# traceback (or a deferred runtime 401) bury what is wrong.
|
|
84
|
+
_render_config_error_banner(config_file, exc)
|
|
85
|
+
raise SystemExit(1) from exc
|
|
46
86
|
config_files.mark_config_loaded(config_file)
|
|
47
87
|
return configuration
|
|
48
88
|
|
|
@@ -16,7 +16,7 @@ import os
|
|
|
16
16
|
from enum import Enum
|
|
17
17
|
from typing import Annotated, Any, Dict, Literal, Optional, Union
|
|
18
18
|
|
|
19
|
-
from pydantic import BaseModel, Field
|
|
19
|
+
from pydantic import BaseModel, Field, model_validator
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class OwnerFilter(str, Enum):
|
|
@@ -81,6 +81,25 @@ class OpenSearchStoreConfig(BaseModel):
|
|
|
81
81
|
secure: bool = Field(default=False, description="Use TLS (https)")
|
|
82
82
|
verify_certs: bool = Field(default=False, description="Verify TLS certs")
|
|
83
83
|
|
|
84
|
+
@model_validator(mode="after")
|
|
85
|
+
def _require_password(self) -> "OpenSearchStoreConfig":
|
|
86
|
+
"""Fail fast at config load if OpenSearch is configured without credentials.
|
|
87
|
+
|
|
88
|
+
Reaching this validator means a ``storage.opensearch`` block is present in
|
|
89
|
+
the active configuration, so the service genuinely depends on OpenSearch.
|
|
90
|
+
A missing password only surfaces later as an opaque HTTP 401 deep inside a
|
|
91
|
+
request handler — we convert it into an actionable startup failure instead.
|
|
92
|
+
"""
|
|
93
|
+
if not self.password:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"OpenSearch is configured (host={self.host!r}, username="
|
|
96
|
+
f"{self.username!r}) but no password was provided. "
|
|
97
|
+
"Set the OPENSEARCH_PASSWORD environment variable (in your .env) "
|
|
98
|
+
"to the OpenSearch password for this user, or remove the "
|
|
99
|
+
"storage.opensearch block if OpenSearch is not used."
|
|
100
|
+
)
|
|
101
|
+
return self
|
|
102
|
+
|
|
84
103
|
|
|
85
104
|
class OpenSearchIndexConfig(BaseModel):
|
|
86
105
|
type: Literal["opensearch"]
|
|
@@ -38,6 +38,7 @@ from fred_core.documents.document_structures import (
|
|
|
38
38
|
Tagging,
|
|
39
39
|
)
|
|
40
40
|
from fred_core.documents.postgres_document_store import PostgresDocumentMetadataStore
|
|
41
|
+
from fred_core.documents.tag_models import TagRow
|
|
41
42
|
|
|
42
43
|
__all__ = [
|
|
43
44
|
# Pydantic models / enums
|
|
@@ -64,4 +65,5 @@ __all__ = [
|
|
|
64
65
|
"DocumentMetadataDeserializationError",
|
|
65
66
|
"DocumentMetadataRow",
|
|
66
67
|
"PostgresDocumentMetadataStore",
|
|
68
|
+
"TagRow",
|
|
67
69
|
]
|
|
@@ -24,8 +24,6 @@ from fred_core.documents.document_structures import DocumentMetadata
|
|
|
24
24
|
class DocumentMetadataDeserializationError(Exception):
|
|
25
25
|
"""Raised when document metadata cannot be parsed correctly due to invalid fields or enum mismatches."""
|
|
26
26
|
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
27
|
|
|
30
28
|
class BaseDocumentMetadataStore:
|
|
31
29
|
"""
|
|
@@ -37,7 +35,6 @@ class BaseDocumentMetadataStore:
|
|
|
37
35
|
@abstractmethod
|
|
38
36
|
async def count_all(self, session: AsyncSession | None = None) -> int:
|
|
39
37
|
"""Return the total number of document metadata records in the store."""
|
|
40
|
-
pass
|
|
41
38
|
|
|
42
39
|
@abstractmethod
|
|
43
40
|
async def get_all_metadata(
|
|
@@ -50,7 +47,6 @@ class BaseDocumentMetadataStore:
|
|
|
50
47
|
- Keys are metadata field names (e.g., "source_tag", "tags")
|
|
51
48
|
- Values are filter values (exact match). Lists are interpreted as 'terms'.
|
|
52
49
|
"""
|
|
53
|
-
pass
|
|
54
50
|
|
|
55
51
|
@abstractmethod
|
|
56
52
|
async def get_metadata_by_uid(
|
|
@@ -61,7 +57,6 @@ class BaseDocumentMetadataStore:
|
|
|
61
57
|
|
|
62
58
|
:raises DocumentMetadataDeserializationError: if stored data is malformed.
|
|
63
59
|
"""
|
|
64
|
-
pass
|
|
65
60
|
|
|
66
61
|
async def get_metadata_by_uids(
|
|
67
62
|
self, document_uids: list[str], session: AsyncSession | None = None
|
|
@@ -88,7 +83,6 @@ class BaseDocumentMetadataStore:
|
|
|
88
83
|
self, tag_id: str, session: AsyncSession | None = None
|
|
89
84
|
) -> List[DocumentMetadata]:
|
|
90
85
|
"""Return all metadata entries that are tagged with a specific tag ID."""
|
|
91
|
-
pass
|
|
92
86
|
|
|
93
87
|
async def browse_metadata_in_tag(
|
|
94
88
|
self,
|
|
@@ -107,7 +101,6 @@ class BaseDocumentMetadataStore:
|
|
|
107
101
|
self, source_tag: str, session: AsyncSession | None = None
|
|
108
102
|
) -> List[DocumentMetadata]:
|
|
109
103
|
"""Return all metadata entries originating from a specific pull source."""
|
|
110
|
-
pass
|
|
111
104
|
|
|
112
105
|
@abstractmethod
|
|
113
106
|
async def save_metadata(
|
|
@@ -121,7 +114,6 @@ class BaseDocumentMetadataStore:
|
|
|
121
114
|
|
|
122
115
|
:raises ValueError: if 'document_uid' is missing.
|
|
123
116
|
"""
|
|
124
|
-
pass
|
|
125
117
|
|
|
126
118
|
@abstractmethod
|
|
127
119
|
async def delete_metadata(
|
|
@@ -132,9 +124,7 @@ class BaseDocumentMetadataStore:
|
|
|
132
124
|
|
|
133
125
|
:raises ValueError: if 'document_uid' is missing.
|
|
134
126
|
"""
|
|
135
|
-
pass
|
|
136
127
|
|
|
137
128
|
@abstractmethod
|
|
138
129
|
async def clear(self, session: AsyncSession | None = None) -> None:
|
|
139
130
|
"""Delete all metadata records from the store. Destructive — dev/test only."""
|
|
140
|
-
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from fred_core.models.base import Base, JsonColumn, TimestampColumn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TagRow(Base):
|
|
12
|
+
"""ORM model for the ``tag`` table — shared between knowledge-flow and the importer."""
|
|
13
|
+
|
|
14
|
+
__tablename__ = "tag"
|
|
15
|
+
|
|
16
|
+
tag_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
17
|
+
created_at: Mapped[datetime | None] = mapped_column(TimestampColumn, nullable=True)
|
|
18
|
+
updated_at: Mapped[datetime | None] = mapped_column(
|
|
19
|
+
TimestampColumn, index=True, nullable=True
|
|
20
|
+
)
|
|
21
|
+
owner_id: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
|
22
|
+
name: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
|
23
|
+
path: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
|
24
|
+
description: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
25
|
+
type: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
|
26
|
+
doc: Mapped[dict | None] = mapped_column(JsonColumn, nullable=True)
|
|
@@ -17,14 +17,13 @@ import re
|
|
|
17
17
|
import time
|
|
18
18
|
from typing import List, Optional, Set
|
|
19
19
|
|
|
20
|
-
from google.cloud import storage
|
|
21
|
-
from google.cloud.exceptions import NotFound
|
|
22
|
-
|
|
23
20
|
from fred_core.filesystem.structures import (
|
|
24
21
|
BaseFilesystem,
|
|
25
22
|
FilesystemResourceInfo,
|
|
26
23
|
FilesystemResourceInfoResult,
|
|
27
24
|
)
|
|
25
|
+
from google.cloud import storage
|
|
26
|
+
from google.cloud.exceptions import NotFound
|
|
28
27
|
|
|
29
28
|
logger = logging.getLogger(__name__)
|
|
30
29
|
|
|
@@ -18,14 +18,13 @@ from io import BytesIO
|
|
|
18
18
|
from typing import List, Set
|
|
19
19
|
from urllib.parse import urlparse
|
|
20
20
|
|
|
21
|
-
from minio import Minio
|
|
22
|
-
from minio.deleteobjects import DeleteObject
|
|
23
|
-
|
|
24
21
|
from fred_core.filesystem.structures import (
|
|
25
22
|
BaseFilesystem,
|
|
26
23
|
FilesystemResourceInfo,
|
|
27
24
|
FilesystemResourceInfoResult,
|
|
28
25
|
)
|
|
26
|
+
from minio import Minio
|
|
27
|
+
from minio.deleteobjects import DeleteObject
|
|
29
28
|
|
|
30
29
|
logger = logging.getLogger(__name__)
|
|
31
30
|
|
|
@@ -148,6 +148,22 @@ class BaseHistoryStore(ABC):
|
|
|
148
148
|
"""
|
|
149
149
|
raise NotImplementedError
|
|
150
150
|
|
|
151
|
+
async def session_exists(
|
|
152
|
+
self,
|
|
153
|
+
session_id: str,
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Return True iff any history row exists for ``session_id`` (any owner).
|
|
157
|
+
|
|
158
|
+
Why this exists:
|
|
159
|
+
- paired with :meth:`session_belongs_to_user`, this lets a caller enforce a
|
|
160
|
+
private-per-owner policy: a session that EXISTS but does not belong to the
|
|
161
|
+
authenticated user must be refused (RUNTIME-07 rev. 2, finding F-C). It
|
|
162
|
+
distinguishes a brand-new session (allow — the caller becomes owner) from
|
|
163
|
+
another user's session (deny).
|
|
164
|
+
"""
|
|
165
|
+
raise NotImplementedError
|
|
166
|
+
|
|
151
167
|
|
|
152
168
|
class NoOpHistoryStore(BaseHistoryStore):
|
|
153
169
|
"""
|
|
@@ -199,3 +215,9 @@ class NoOpHistoryStore(BaseHistoryStore):
|
|
|
199
215
|
user_id: str,
|
|
200
216
|
) -> bool:
|
|
201
217
|
return False
|
|
218
|
+
|
|
219
|
+
async def session_exists(
|
|
220
|
+
self,
|
|
221
|
+
session_id: str,
|
|
222
|
+
) -> bool:
|
|
223
|
+
return False
|
|
@@ -368,6 +368,21 @@ class PostgresHistoryStore(BaseHistoryStore):
|
|
|
368
368
|
)
|
|
369
369
|
return (result.scalar() or 0) > 0
|
|
370
370
|
|
|
371
|
+
async def session_exists(
|
|
372
|
+
self,
|
|
373
|
+
session_id: str,
|
|
374
|
+
session: AsyncSession | None = None,
|
|
375
|
+
) -> bool:
|
|
376
|
+
"""Return True iff any history row exists for ``session_id`` (any owner)."""
|
|
377
|
+
await self._ensure_tables()
|
|
378
|
+
async with use_session(self._sessions, session) as s:
|
|
379
|
+
result = await s.execute(
|
|
380
|
+
select(func.count()).where(
|
|
381
|
+
SessionHistoryRow.session_id == session_id,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
return (result.scalar() or 0) > 0
|
|
385
|
+
|
|
371
386
|
# ------------------------------------------------------------------
|
|
372
387
|
# Rank helper
|
|
373
388
|
# ------------------------------------------------------------------
|
|
@@ -64,6 +64,10 @@ KPI_INDEX_MAPPING: Dict[str, Any] = {
|
|
|
64
64
|
"env": {"type": "keyword"},
|
|
65
65
|
"cluster": {"type": "keyword"},
|
|
66
66
|
"user_id": {"type": "keyword"},
|
|
67
|
+
# Conversation id carried by every session-scoped KPI row
|
|
68
|
+
# (react/graph phase, tool). Explicit keyword so the RGPD
|
|
69
|
+
# erasure `update_by_query` can `term`-match it (CTRLP-12 A3).
|
|
70
|
+
"session_id": {"type": "keyword"},
|
|
67
71
|
"exchange_id": {"type": "keyword"},
|
|
68
72
|
"agent_id": {"type": "keyword"},
|
|
69
73
|
"agent_step": {"type": "keyword"},
|
|
@@ -170,6 +174,11 @@ class OpenSearchKPIStore(BaseKPIStore):
|
|
|
170
174
|
logger.info("[OPENSEARCH][KPI] created index '%s'", self.index)
|
|
171
175
|
else:
|
|
172
176
|
logger.info("[OPENSEARCH][KPI] index '%s' already exists.", self.index)
|
|
177
|
+
# CTRLP-12: session_id was added to KPI_INDEX_MAPPING for RGPD
|
|
178
|
+
# conversation erasure. Existing indices predate it, so add it
|
|
179
|
+
# additively (put_mapping) before validation — otherwise startup
|
|
180
|
+
# fails with "Missing nested field: 'dims.session_id'".
|
|
181
|
+
self._ensure_dim_mapping("session_id", {"type": "keyword"})
|
|
173
182
|
self._ensure_dim_mapping("service", {"type": "keyword"})
|
|
174
183
|
self._ensure_dim_mapping("team_id", {"type": "keyword"})
|
|
175
184
|
self._ensure_dim_mapping("agent_instance_id", {"type": "keyword"})
|
|
@@ -234,6 +243,57 @@ class OpenSearchKPIStore(BaseKPIStore):
|
|
|
234
243
|
logger.error("[OPENSEARCH][KPI] bulk_index failed: %s", e)
|
|
235
244
|
raise
|
|
236
245
|
|
|
246
|
+
# -- erasure (RGPD) --------------------------------------------------------
|
|
247
|
+
def anonymise_for_session(self, session_id: str) -> int:
|
|
248
|
+
"""Anonymise — not delete — every KPI row of one conversation (CTRLP-12 A3).
|
|
249
|
+
|
|
250
|
+
RGPD default per RFC §3.3: KPI events are an analytics *aggregate*, not
|
|
251
|
+
conversation content, so erasure severs the link to a person rather than
|
|
252
|
+
dropping the rows. An OpenSearch `update_by_query` (the same primitive the
|
|
253
|
+
vector store uses for `delete_by_query`) nulls the direct identifiers on
|
|
254
|
+
the session's rows: `dims.session_id`, `dims.user_id`, `dims.exchange_id`
|
|
255
|
+
and — for any generic session-scoped analytics row — `dims.scope_id`.
|
|
256
|
+
Aggregate counts are untouched — the rows survive, now anonymous. Returns
|
|
257
|
+
the number of rows anonymised.
|
|
258
|
+
|
|
259
|
+
A session's rows are matched by `dims.session_id == session_id`, the dim
|
|
260
|
+
every session-scoped KPI row actually carries (react/graph `phase_latency`
|
|
261
|
+
and `tool_*` all set `dims.session_id`; see the runtime KPI emitters). A
|
|
262
|
+
pure aggregate with no `session_id` (e.g. `llm.call_latency_ms` emitted
|
|
263
|
+
under a system actor) carries no personal identifier and is correctly out
|
|
264
|
+
of per-conversation scope. Idempotent: once `session_id` is nulled the row
|
|
265
|
+
no longer matches, so a second erase updates zero.
|
|
266
|
+
"""
|
|
267
|
+
body: Dict[str, Any] = {
|
|
268
|
+
"query": {"bool": {"filter": [{"term": {"dims.session_id": session_id}}]}},
|
|
269
|
+
"script": {
|
|
270
|
+
"lang": "painless",
|
|
271
|
+
"source": (
|
|
272
|
+
"if (ctx._source.dims != null) "
|
|
273
|
+
"{ for (f in params.fields) { ctx._source.dims.remove(f); } }"
|
|
274
|
+
),
|
|
275
|
+
"params": {
|
|
276
|
+
"fields": ["session_id", "scope_id", "user_id", "exchange_id"]
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
try:
|
|
281
|
+
resp = self.client.update_by_query(
|
|
282
|
+
index=self.index,
|
|
283
|
+
body=body,
|
|
284
|
+
params={"conflicts": "proceed", "refresh": "true"},
|
|
285
|
+
)
|
|
286
|
+
updated = int(resp.get("updated", 0))
|
|
287
|
+
logger.info(
|
|
288
|
+
"[OPENSEARCH][KPI] anonymised %s row(s) for session_id=%s",
|
|
289
|
+
updated,
|
|
290
|
+
session_id,
|
|
291
|
+
)
|
|
292
|
+
return updated
|
|
293
|
+
except OpenSearchException as e:
|
|
294
|
+
logger.error("[OPENSEARCH][KPI] anonymise_for_session failed: %s", e)
|
|
295
|
+
raise
|
|
296
|
+
|
|
237
297
|
# -- reads -----------------------------------------------------------------
|
|
238
298
|
def query(self, q: KPIQuery) -> KPIQueryResult:
|
|
239
299
|
logger.info(
|
|
@@ -81,7 +81,7 @@ class StoreEmitHandler(logging.Handler):
|
|
|
81
81
|
payload = json.loads(raw)
|
|
82
82
|
except Exception:
|
|
83
83
|
print("Log record is not JSON: %s", raw)
|
|
84
|
-
|
|
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")
|
|
@@ -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
|
+
)
|