fred-runtime 3.3.0__tar.gz → 3.3.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_runtime-3.3.0 → fred_runtime-3.3.2}/PKG-INFO +4 -4
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/agent_app.py +119 -6
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/mcp_runtime.py +112 -18
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/mcp_utils.py +17 -8
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/deep/deep_runtime.py +12 -6
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/graph/graph_runtime.py +0 -1
- fred_runtime-3.3.2/fred_runtime/integrations/inprocess_toolkit_registry.py +50 -0
- fred_runtime-3.3.2/fred_runtime/integrations/kf_vector_search/__init__.py +21 -0
- fred_runtime-3.3.2/fred_runtime/integrations/kf_vector_search/toolkit.py +186 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/integrations/v2_runtime/adapters.py +1 -2
- fred_runtime-3.3.2/fred_runtime/react/react_prompting.py +310 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_runtime.py +7 -6
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_context.py +3 -2
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/sql_checkpointer.py +219 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/PKG-INFO +4 -4
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/SOURCES.txt +7 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/requires.txt +3 -3
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/pyproject.toml +4 -4
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_agent_app.py +261 -19
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_client.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_config_loader.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_context.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_context_aware_tool.py +1 -2
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_conversational_memory.py +5 -10
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_eval_trace.py +1 -2
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_fred_workspace_fs.py +2 -3
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_graph_runtime_invoke_agent.py +5 -6
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_graph_runtime_observability.py +1 -2
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_history.py +5 -6
- fred_runtime-3.3.2/tests/test_kf_vector_search_toolkit.py +136 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_kf_workspace_client.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_mcp_config.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_model_routing.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_openai_compat_router.py +2 -3
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_pod_client.py +0 -1
- fred_runtime-3.3.2/tests/test_react_prompting.py +216 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_react_thinking.py +13 -14
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_repl_helpers.py +0 -1
- fred_runtime-3.3.2/tests/test_runtime_context_prompt_injection.py +164 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_smoke.py +5 -6
- fred_runtime-3.3.2/tests/test_sql_checkpointer_owner.py +271 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_token_expiry.py +0 -1
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_user_token_refresher.py +0 -1
- fred_runtime-3.3.0/fred_runtime/react/react_prompting.py +0 -164
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/README.md +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/config.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/context.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/app/openai_compat_router.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_logs_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/support/tool_approval.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime/support/tool_loop.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/setup.cfg +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.3.0 → fred_runtime-3.3.2}/tests/test_url_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-runtime
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
4
|
Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
|
|
5
5
|
Author-email: Thales <noreply@thalesgroup.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,8 +12,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: <3.13,>=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: fred-core>=3.4.
|
|
16
|
-
Requires-Dist: fred-sdk>=3.3.
|
|
15
|
+
Requires-Dist: fred-core>=3.4.1
|
|
16
|
+
Requires-Dist: fred-sdk>=3.3.1
|
|
17
17
|
Requires-Dist: alembic>=1.18.4
|
|
18
18
|
Requires-Dist: deepagents>=0.4.11
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -25,7 +25,7 @@ Requires-Dist: langfuse>=3.0.0
|
|
|
25
25
|
Requires-Dist: requests<3,>=2.32
|
|
26
26
|
Requires-Dist: PyYAML>=6.0
|
|
27
27
|
Provides-Extra: app
|
|
28
|
-
Requires-Dist: fastapi>=0.
|
|
28
|
+
Requires-Dist: fastapi>=0.139.0; extra == "app"
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: bandit>=1.8.6; extra == "dev"
|
|
31
31
|
Requires-Dist: basedpyright==1.31.0; extra == "dev"
|
|
@@ -56,6 +56,7 @@ from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, status
|
|
|
56
56
|
from fastapi.middleware.cors import CORSMiddleware
|
|
57
57
|
from fastapi.responses import StreamingResponse
|
|
58
58
|
from fred_core.common.config_loader import get_config
|
|
59
|
+
from fred_core.common.team_id import is_personal_team_id, personal_team_id
|
|
59
60
|
from fred_core.history.history_schema import ChatMessage
|
|
60
61
|
from fred_core.kpi import KPIMiddleware
|
|
61
62
|
from fred_core.kpi.kpi_writer_structures import KPIActor
|
|
@@ -63,9 +64,13 @@ from fred_core.logs.log_setup import log_setup
|
|
|
63
64
|
from fred_core.logs.memory_log_store import RamLogStore
|
|
64
65
|
from fred_core.security.models import AuthorizationError
|
|
65
66
|
from fred_core.security.oidc import get_keycloak_client_id, get_keycloak_url
|
|
66
|
-
from fred_core.security.rebac.rebac_engine import
|
|
67
|
+
from fred_core.security.rebac.rebac_engine import (
|
|
68
|
+
ORGANIZATION_ID,
|
|
69
|
+
OrganizationPermission,
|
|
70
|
+
TeamPermission,
|
|
71
|
+
)
|
|
67
72
|
from fred_core.security.rebac.rebac_factory import rebac_factory
|
|
68
|
-
from fred_core.security.structure import KeycloakUser
|
|
73
|
+
from fred_core.security.structure import KeycloakUser, is_service_agent
|
|
69
74
|
from fred_sdk.contracts.context import (
|
|
70
75
|
AgentInvocationRequest,
|
|
71
76
|
AgentInvocationResult,
|
|
@@ -110,6 +115,7 @@ from fred_runtime.react.react_runtime import ReActRuntime
|
|
|
110
115
|
from fred_runtime.runtime_support.checkpoints import load_checkpoint
|
|
111
116
|
|
|
112
117
|
from ..common.structures import AgentSettingsLike
|
|
118
|
+
from ..integrations.inprocess_toolkit_registry import build_inprocess_toolkit
|
|
113
119
|
from ..integrations.v2_runtime.adapters import (
|
|
114
120
|
CompositeToolInvoker,
|
|
115
121
|
FredKnowledgeSearchToolInvoker,
|
|
@@ -1122,6 +1128,12 @@ async def _authorize_execution_or_raise(
|
|
|
1122
1128
|
- security disabled (no authenticated user) → skip (dev/local).
|
|
1123
1129
|
- ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
|
|
1124
1130
|
the C3 profile guarantees an enabled engine in classified deployments.
|
|
1131
|
+
- a personal space (`personal-<uid>`) is intrinsic ownership, not a stored
|
|
1132
|
+
OpenFGA tuple: a human caller acting on their own canonical
|
|
1133
|
+
`personal_team_id(authenticated_user.uid)` is authorized without an OpenFGA
|
|
1134
|
+
call; any other `personal-*` id (another user's space, or the bare
|
|
1135
|
+
`"personal"` alias) is explicitly denied — never routed to OpenFGA (AUTHZ-05
|
|
1136
|
+
item 8b watch item).
|
|
1125
1137
|
- otherwise require the caller to hold `CAN_READ` on the requested team — the
|
|
1126
1138
|
same relation the control-plane required before it would mint a grant. The
|
|
1127
1139
|
team is caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
@@ -1183,6 +1195,50 @@ async def _authorize_execution_or_raise(
|
|
|
1183
1195
|
"(runtime_context.team_id)"
|
|
1184
1196
|
),
|
|
1185
1197
|
)
|
|
1198
|
+
if is_service_agent(authenticated_user):
|
|
1199
|
+
# Solution A (RFC EVAL-AUTH): a service identity (the evaluation worker) is
|
|
1200
|
+
# recognized for execution, scoped to the request team_id, without any stored
|
|
1201
|
+
# OpenFGA relation. Legitimacy is anchored upstream at campaign creation. The
|
|
1202
|
+
# team_id is guaranteed non-None above, so this stays team-scoped (never global).
|
|
1203
|
+
_emit_audit_event(
|
|
1204
|
+
container,
|
|
1205
|
+
"info",
|
|
1206
|
+
"service_agent_authorized",
|
|
1207
|
+
user_id=authenticated_user.uid,
|
|
1208
|
+
team_id=team_id,
|
|
1209
|
+
agent_instance_id=request.agent_instance_id,
|
|
1210
|
+
)
|
|
1211
|
+
return
|
|
1212
|
+
if team_id == "personal" or is_personal_team_id(team_id):
|
|
1213
|
+
# Personal spaces are intrinsic ownership (AUTHZ-05 item 8b): the id is
|
|
1214
|
+
# derived from the JWT subject via `personal_team_id`, is never persisted
|
|
1215
|
+
# as an OpenFGA tuple, and must never be resolved by OpenFGA. Only an
|
|
1216
|
+
# exact match against the caller's own canonical id is authorized; any
|
|
1217
|
+
# other personal-* id (another user's space) or the bare "personal"
|
|
1218
|
+
# alias (ambiguous once ReBAC is active) is denied here, before OpenFGA
|
|
1219
|
+
# ever sees it.
|
|
1220
|
+
if team_id == personal_team_id(authenticated_user.uid):
|
|
1221
|
+
_emit_audit_event(
|
|
1222
|
+
container,
|
|
1223
|
+
"info",
|
|
1224
|
+
"personal_space_owner_authorized",
|
|
1225
|
+
user_id=authenticated_user.uid,
|
|
1226
|
+
team_id=team_id,
|
|
1227
|
+
agent_instance_id=request.agent_instance_id,
|
|
1228
|
+
)
|
|
1229
|
+
return
|
|
1230
|
+
_emit_audit_event(
|
|
1231
|
+
container,
|
|
1232
|
+
"warning",
|
|
1233
|
+
"personal_space_denied",
|
|
1234
|
+
user_id=authenticated_user.uid,
|
|
1235
|
+
team_id=team_id,
|
|
1236
|
+
agent_instance_id=request.agent_instance_id,
|
|
1237
|
+
)
|
|
1238
|
+
raise HTTPException(
|
|
1239
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1240
|
+
detail=f"user {authenticated_user.uid!r} is not authorized for team {team_id!r}",
|
|
1241
|
+
)
|
|
1186
1242
|
try:
|
|
1187
1243
|
await rebac.check_user_team_permission_or_raise(
|
|
1188
1244
|
authenticated_user, TeamPermission.CAN_READ, team_id
|
|
@@ -1255,6 +1311,31 @@ async def _enforce_session_ownership(
|
|
|
1255
1311
|
)
|
|
1256
1312
|
|
|
1257
1313
|
|
|
1314
|
+
async def _caller_can_manage_platform(caller: KeycloakUser | None) -> bool:
|
|
1315
|
+
"""True when the caller holds org-level ``can_manage_platform`` (CTRLP-12 C1).
|
|
1316
|
+
|
|
1317
|
+
The delete endpoints use this as an **admin branch**: a platform service
|
|
1318
|
+
principal — e.g. the control-plane lifecycle worker erasing a conversation at
|
|
1319
|
+
window expiry — may delete a session it does not personally own. Only the
|
|
1320
|
+
per-user *ownership* check is waived; *authentication* stays enforced upstream
|
|
1321
|
+
by ``_authenticated_user``.
|
|
1322
|
+
|
|
1323
|
+
Fails closed: returns False when there is no caller or when ReBAC is disabled
|
|
1324
|
+
(dev/no-security), so ownership behaviour is unchanged there and the bypass
|
|
1325
|
+
activates only when ReBAC is actually enforcing and the org permission is
|
|
1326
|
+
granted. Reuses the AUTHZ-01 ``can_manage_platform`` permission — no second
|
|
1327
|
+
bypass is forked.
|
|
1328
|
+
"""
|
|
1329
|
+
if caller is None:
|
|
1330
|
+
return False
|
|
1331
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
1332
|
+
if rebac is None or not rebac.enabled:
|
|
1333
|
+
return False
|
|
1334
|
+
return await rebac.has_user_permission(
|
|
1335
|
+
caller, OrganizationPermission.CAN_MANAGE_PLATFORM, ORGANIZATION_ID
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
|
|
1258
1339
|
async def _authorize_and_resolve(
|
|
1259
1340
|
request: RuntimeExecuteRequest,
|
|
1260
1341
|
*,
|
|
@@ -2010,7 +2091,6 @@ async def _iterate_runtime_event_payloads(
|
|
|
2010
2091
|
checkpoint_id=resolved_checkpoint_id,
|
|
2011
2092
|
user_id=ctx.get("user_id"),
|
|
2012
2093
|
team_id=resolved_team_id,
|
|
2013
|
-
user_groups=ctx.get("user_groups"),
|
|
2014
2094
|
language=ctx.get("language"),
|
|
2015
2095
|
access_token=access_token,
|
|
2016
2096
|
refresh_token=ctx.get("refresh_token"),
|
|
@@ -2036,6 +2116,10 @@ async def _iterate_runtime_event_payloads(
|
|
|
2036
2116
|
# at prepare-execution and the frontend forwards it — but it was also
|
|
2037
2117
|
# silently dropped here, so no agent ever received a selected prompt.
|
|
2038
2118
|
context_prompt_text=ctx.get("context_prompt_text"),
|
|
2119
|
+
# Rebuilt by the frontend from the attachments that currently exist.
|
|
2120
|
+
# When the final file is deleted this is absent, so the per-turn runtime
|
|
2121
|
+
# notice disappears without leaving a checkpointed system message behind.
|
|
2122
|
+
attachments_markdown=ctx.get("attachments_markdown"),
|
|
2039
2123
|
)
|
|
2040
2124
|
|
|
2041
2125
|
binding = BoundRuntimeContext(
|
|
@@ -2195,6 +2279,7 @@ def _build_agent_router(
|
|
|
2195
2279
|
async def get_kpi_turns(
|
|
2196
2280
|
limit: int = 50,
|
|
2197
2281
|
container: PodApplicationContext = Depends(get_pod_container),
|
|
2282
|
+
caller: KeycloakUser | None = Depends(_authenticated_user),
|
|
2198
2283
|
) -> list[KpiTurnRecord]:
|
|
2199
2284
|
"""
|
|
2200
2285
|
Return recent agent.turn_completed KPI events, newest first.
|
|
@@ -2209,7 +2294,20 @@ def _build_agent_router(
|
|
|
2209
2294
|
- developers need to validate KPI emission from the CLI without
|
|
2210
2295
|
Grafana or Prometheus; this exposes the pod-local ring buffer
|
|
2211
2296
|
- max 200 entries retained in memory (oldest evicted automatically)
|
|
2297
|
+
|
|
2298
|
+
AUTHZ-05 review finding: item 8a's org-level CAN_READ_METRICS removal
|
|
2299
|
+
does not apply here — unlike the tier-2 capability it replaced
|
|
2300
|
+
(near-universal, protected nothing specific), this buffer holds
|
|
2301
|
+
cross-user/cross-team data (session_id, exchange_id, user_id, team_id,
|
|
2302
|
+
token counts) for every caller that has hit this pod, not just the
|
|
2303
|
+
caller's own. Gated the same way as the sibling `get_audit_events`
|
|
2304
|
+
below, on `CAN_MANAGE_PLATFORM`, not deleted outright.
|
|
2212
2305
|
"""
|
|
2306
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
2307
|
+
if caller is not None and rebac is not None and rebac.enabled:
|
|
2308
|
+
await rebac.check_user_permission_or_raise(
|
|
2309
|
+
caller, OrganizationPermission.CAN_MANAGE_PLATFORM, ORGANIZATION_ID
|
|
2310
|
+
)
|
|
2213
2311
|
with container._kpi_turns_lock:
|
|
2214
2312
|
events = list(container.kpi_turns_buffer)
|
|
2215
2313
|
events.reverse()
|
|
@@ -2219,6 +2317,7 @@ def _build_agent_router(
|
|
|
2219
2317
|
async def get_audit_events(
|
|
2220
2318
|
limit: int = 50,
|
|
2221
2319
|
container: PodApplicationContext = Depends(get_pod_container),
|
|
2320
|
+
caller: KeycloakUser | None = Depends(_authenticated_user),
|
|
2222
2321
|
) -> list[AuditEventRecord]:
|
|
2223
2322
|
"""
|
|
2224
2323
|
Return recent security audit events, newest first.
|
|
@@ -2233,6 +2332,11 @@ def _build_agent_router(
|
|
|
2233
2332
|
this ring buffer so the CLI can query them directly
|
|
2234
2333
|
- max 200 entries retained in memory
|
|
2235
2334
|
"""
|
|
2335
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
2336
|
+
if caller is not None and rebac is not None and rebac.enabled:
|
|
2337
|
+
await rebac.check_user_permission_or_raise(
|
|
2338
|
+
caller, OrganizationPermission.CAN_MANAGE_PLATFORM, ORGANIZATION_ID
|
|
2339
|
+
)
|
|
2236
2340
|
with container._audit_events_lock:
|
|
2237
2341
|
events = list(container.audit_events_buffer)
|
|
2238
2342
|
events.reverse()
|
|
@@ -2392,6 +2496,11 @@ def _build_agent_router(
|
|
|
2392
2496
|
detail="No history store configured — session history is unavailable.",
|
|
2393
2497
|
)
|
|
2394
2498
|
caller_uid = caller.uid if caller is not None else None
|
|
2499
|
+
if await _caller_can_manage_platform(caller):
|
|
2500
|
+
# Platform admin (e.g. CTRLP-12 lifecycle erase-at-expiry): delete every
|
|
2501
|
+
# row for the session, not just the caller's — ownership is waived,
|
|
2502
|
+
# authentication stays enforced upstream.
|
|
2503
|
+
caller_uid = None
|
|
2395
2504
|
count = await history_store.delete_session(
|
|
2396
2505
|
session_id=session_id, user_id=caller_uid
|
|
2397
2506
|
)
|
|
@@ -2717,7 +2826,8 @@ def _build_agent_router(
|
|
|
2717
2826
|
caller_uid = caller.uid if caller is not None else None
|
|
2718
2827
|
history_store = _get_history_store_for_owned_access(caller)
|
|
2719
2828
|
if (
|
|
2720
|
-
|
|
2829
|
+
not await _caller_can_manage_platform(caller)
|
|
2830
|
+
and caller_uid is not None
|
|
2721
2831
|
and history_store is not None
|
|
2722
2832
|
and not await history_store.session_belongs_to_user(session_id, caller_uid)
|
|
2723
2833
|
):
|
|
@@ -2925,8 +3035,10 @@ def _build_agent_router(
|
|
|
2925
3035
|
stream closes. Do not rely on it as an end-of-turn signal here.
|
|
2926
3036
|
|
|
2927
3037
|
Security:
|
|
2928
|
-
-
|
|
2929
|
-
|
|
3038
|
+
- There is no ExecutionGrant. Identity is the caller's Keycloak JWT; for
|
|
3039
|
+
managed execution (agent_instance_id) the runtime authorizes each request
|
|
3040
|
+
itself with a per-request ReBAC (OpenFGA) check on runtime_context.team_id.
|
|
3041
|
+
The control plane issues no signed grant or capability token.
|
|
2930
3042
|
- RBAC via Keycloak and REBAC via OpenFGA protect this endpoint.
|
|
2931
3043
|
|
|
2932
3044
|
Architectural note:
|
|
@@ -3073,6 +3185,7 @@ def create_agent_app(
|
|
|
3073
3185
|
checkpointer=checkpointer,
|
|
3074
3186
|
history_store=history_store,
|
|
3075
3187
|
mcp_configuration=config.get_mcp_configuration(),
|
|
3188
|
+
inprocess_toolkit_factory=build_inprocess_toolkit,
|
|
3076
3189
|
control_plane_url=config.platform.control_plane_url,
|
|
3077
3190
|
rebac_engine=rebac_engine,
|
|
3078
3191
|
security_profile=(
|
|
@@ -18,8 +18,9 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import logging
|
|
21
|
+
import time
|
|
21
22
|
from collections.abc import Awaitable, Iterable
|
|
22
|
-
from typing import Any, Callable, List, Optional, cast
|
|
23
|
+
from typing import Any, Callable, List, Optional, Tuple, cast
|
|
23
24
|
|
|
24
25
|
from fred_sdk.contracts.context import RuntimeContext as AgentRuntimeContext
|
|
25
26
|
from fred_sdk.contracts.models import (
|
|
@@ -45,6 +46,105 @@ logger = logging.getLogger(__name__)
|
|
|
45
46
|
MCP_CONNECT_MAX_ATTEMPTS = 3
|
|
46
47
|
MCP_CONNECT_RETRY_BASE_DELAY_SECS = 0.5
|
|
47
48
|
|
|
49
|
+
# --- Cross-request connect/tool-list cache (perf) -----------------------------
|
|
50
|
+
# BENCH-<pending id>: `_run_lifecycle` used to reconnect + re-list tools against
|
|
51
|
+
# every configured MCP server on EVERY agent turn (measured: ~2-3s wall time for
|
|
52
|
+
# a 7-server agent, with a near-instant mock LLM). `MultiServerMCPClient` itself
|
|
53
|
+
# holds no persistent resources (see `_close_mcp_client_quietly` below), so the
|
|
54
|
+
# per-turn cost was purely the repeated connect+list_tools round trips, not
|
|
55
|
+
# anything a "closed" connection would have saved by staying open.
|
|
56
|
+
#
|
|
57
|
+
# This cache reuses the (client, tools) pair across turns for the SAME agent +
|
|
58
|
+
# server set + access token, so repeat turns in a session (or across sessions
|
|
59
|
+
# for the same user/dev token) skip the round trips entirely.
|
|
60
|
+
#
|
|
61
|
+
# Auth freshness on a cache hit: `langchain_mcp_adapters` reads `tool_interceptors`
|
|
62
|
+
# (and per-server `headers`) live off the client instance at call time, not from a
|
|
63
|
+
# snapshot frozen at discovery time (verified against langchain_mcp_adapters==0.3.0
|
|
64
|
+
# — see `execute_tool`/`_build_interceptor_chain` in its `tools.py`). So on a cache
|
|
65
|
+
# hit we mutate `client.tool_interceptors` IN PLACE (never reassign the list) to the
|
|
66
|
+
# CURRENT request's own interceptors before handing the client back — every request
|
|
67
|
+
# still gets its own live `ExpiredTokenRetryInterceptor` (bound to ITS OWN, live
|
|
68
|
+
# `agent_instance`/token-refresh callback), even though the underlying connection +
|
|
69
|
+
# discovered tool schemas are shared. Headers don't need the same treatment: the
|
|
70
|
+
# cache key already includes the exact access_token string, so a cache hit implies
|
|
71
|
+
# an identical token/header value already.
|
|
72
|
+
#
|
|
73
|
+
# Residual, accepted race: two truly concurrent requests for the SAME identity
|
|
74
|
+
# (same cache key) could have request A's in-flight tool call pick up request B's
|
|
75
|
+
# interceptor instance instead of its own. Both point at the same live user, so
|
|
76
|
+
# this is never a cross-user leak — at worst a harmless same-identity mix-up.
|
|
77
|
+
# Closing it fully would need a lock held for an entire turn's tool-calling phase,
|
|
78
|
+
# which would serialize concurrent turns for the same user; not worth it.
|
|
79
|
+
MCP_CLIENT_CACHE_TTL_SECS = 300.0
|
|
80
|
+
_mcp_client_cache: dict[
|
|
81
|
+
Tuple[str, Tuple[str, ...], str], Tuple[MultiServerMCPClient, List[BaseTool], float]
|
|
82
|
+
] = {}
|
|
83
|
+
_mcp_client_cache_lock = asyncio.Lock()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _mcp_cache_key(
|
|
87
|
+
agent_id: str, servers: List[MCPServerConfiguration], access_token: str | None
|
|
88
|
+
) -> Tuple[str, Tuple[str, ...], str]:
|
|
89
|
+
return (agent_id, tuple(sorted(s.id for s in servers)), access_token or "")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _prune_expired_mcp_cache_locked(now: float) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Drop every expired entry from `_mcp_client_cache`.
|
|
95
|
+
|
|
96
|
+
Why this exists:
|
|
97
|
+
- a cache hit only checks the ONE key being looked up; without this, any
|
|
98
|
+
OTHER key that expires (e.g. a rotated user token, an agent that stops
|
|
99
|
+
being used) stays resident in process memory forever — a real memory
|
|
100
|
+
leak over a long-running pod's life, and it keeps stale `Authorization`
|
|
101
|
+
headers (the cache key embeds the raw access_token) around indefinitely.
|
|
102
|
+
|
|
103
|
+
How to use it:
|
|
104
|
+
- call under `_mcp_client_cache_lock`, before a lookup or insert.
|
|
105
|
+
"""
|
|
106
|
+
expired = [k for k, v in _mcp_client_cache.items() if v[2] <= now]
|
|
107
|
+
for k in expired:
|
|
108
|
+
del _mcp_client_cache[k]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _get_or_connect_mcp_client(
|
|
112
|
+
*,
|
|
113
|
+
agent_id: str,
|
|
114
|
+
mcp_servers: List[MCPServerConfiguration],
|
|
115
|
+
runtime_context: AgentRuntimeContext,
|
|
116
|
+
tool_interceptors: list,
|
|
117
|
+
) -> Tuple[MultiServerMCPClient, List[BaseTool]]:
|
|
118
|
+
key = _mcp_cache_key(agent_id, mcp_servers, runtime_context.access_token)
|
|
119
|
+
now = time.monotonic()
|
|
120
|
+
|
|
121
|
+
async with _mcp_client_cache_lock:
|
|
122
|
+
_prune_expired_mcp_cache_locked(now)
|
|
123
|
+
cached = _mcp_client_cache.get(key)
|
|
124
|
+
if cached is not None and cached[2] > now:
|
|
125
|
+
cached_client = cached[0]
|
|
126
|
+
# In-place mutation only — reassigning `.tool_interceptors` would not
|
|
127
|
+
# be seen by tool closures already vended from a prior get_tools()
|
|
128
|
+
# call (they hold a reference to the original list object).
|
|
129
|
+
cached_client.tool_interceptors[:] = tool_interceptors
|
|
130
|
+
logger.info(
|
|
131
|
+
"[MCP] agent=%s cache hit: reusing connected client (%d tools), "
|
|
132
|
+
"current request's interceptors swapped in.",
|
|
133
|
+
agent_id,
|
|
134
|
+
len(cached[1]),
|
|
135
|
+
)
|
|
136
|
+
return cached_client, cached[1]
|
|
137
|
+
|
|
138
|
+
client, tools = await get_connected_mcp_client_for_agent(
|
|
139
|
+
agent_id=agent_id,
|
|
140
|
+
mcp_servers=mcp_servers,
|
|
141
|
+
runtime_context=runtime_context,
|
|
142
|
+
tool_interceptors=tool_interceptors,
|
|
143
|
+
)
|
|
144
|
+
async with _mcp_client_cache_lock:
|
|
145
|
+
_mcp_client_cache[key] = (client, tools, now + MCP_CLIENT_CACHE_TTL_SECS)
|
|
146
|
+
return client, tools
|
|
147
|
+
|
|
48
148
|
|
|
49
149
|
async def _close_mcp_client_quietly(client: Optional[MultiServerMCPClient]) -> None:
|
|
50
150
|
if not client:
|
|
@@ -245,29 +345,23 @@ class MCPRuntime:
|
|
|
245
345
|
if refresh_cb:
|
|
246
346
|
interceptors.append(ExpiredTokenRetryInterceptor(refresh_cb))
|
|
247
347
|
|
|
248
|
-
new_client = await
|
|
348
|
+
new_client, tools = await _get_or_connect_mcp_client(
|
|
249
349
|
agent_id=self._agent_id,
|
|
250
350
|
mcp_servers=self.remote_servers,
|
|
251
351
|
runtime_context=runtime_context,
|
|
252
352
|
tool_interceptors=interceptors,
|
|
253
353
|
)
|
|
254
354
|
self.mcp_client = new_client
|
|
355
|
+
# `tools` were already fetched (per server) while connecting/validating
|
|
356
|
+
# above — reuse them instead of paying for a second get_tools() round
|
|
357
|
+
# trip against every server on every single turn.
|
|
255
358
|
self.toolkit = McpToolkit(client=new_client, agent=self.agent_instance)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
self.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
self._agent_id,
|
|
263
|
-
len(tools),
|
|
264
|
-
)
|
|
265
|
-
except Exception:
|
|
266
|
-
logger.warning(
|
|
267
|
-
"[MCP] agent=%s init: Failed to prefetch tools; toolkit will attempt best-effort discovery later.",
|
|
268
|
-
self._agent_id,
|
|
269
|
-
exc_info=True,
|
|
270
|
-
)
|
|
359
|
+
self.toolkit.tools = tools
|
|
360
|
+
logger.info(
|
|
361
|
+
"[MCP] agent=%s init: Connected and cached %d tools.",
|
|
362
|
+
self._agent_id,
|
|
363
|
+
len(tools),
|
|
364
|
+
)
|
|
271
365
|
logger.info(
|
|
272
366
|
"[MCP] agent=%s init: Successfully built and connected client.",
|
|
273
367
|
self._agent_id,
|
|
@@ -375,7 +469,7 @@ class MCPRuntime:
|
|
|
375
469
|
)
|
|
376
470
|
return
|
|
377
471
|
for server in self.inprocess_servers:
|
|
378
|
-
toolkit = factory(server.provider)
|
|
472
|
+
toolkit = factory(server.provider, self.agent_instance)
|
|
379
473
|
if toolkit is None:
|
|
380
474
|
logger.warning(
|
|
381
475
|
"[MCP] agent=%s no toolkit built for provider=%s (server=%s)",
|
|
@@ -25,7 +25,8 @@ Contract
|
|
|
25
25
|
--------
|
|
26
26
|
- Requires an `access_token_provider` (a callable) to fetch the user's token
|
|
27
27
|
before connecting to any MCP server.
|
|
28
|
-
- Returns a connected `MultiServerMCPClient
|
|
28
|
+
- Returns a connected `MultiServerMCPClient` plus the tools already fetched
|
|
29
|
+
while validating each server (see `get_connected_mcp_client_for_agent`).
|
|
29
30
|
- Raises `ExceptionGroup` if **any** server fails to connect.
|
|
30
31
|
|
|
31
32
|
"""
|
|
@@ -35,10 +36,11 @@ from __future__ import annotations
|
|
|
35
36
|
import logging
|
|
36
37
|
import time
|
|
37
38
|
from datetime import timedelta
|
|
38
|
-
from typing import Dict, List
|
|
39
|
+
from typing import Dict, List, Tuple
|
|
39
40
|
|
|
40
41
|
from fred_sdk.contracts.context import RuntimeContext
|
|
41
42
|
from fred_sdk.contracts.models import MCPServerConfiguration
|
|
43
|
+
from langchain_core.tools import BaseTool
|
|
42
44
|
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
43
45
|
from langchain_mcp_adapters.sessions import Connection, StreamableHttpConnection
|
|
44
46
|
|
|
@@ -184,10 +186,14 @@ async def get_connected_mcp_client_for_agent(
|
|
|
184
186
|
*,
|
|
185
187
|
tool_interceptors: list | None = None,
|
|
186
188
|
# -----------------------------------------------
|
|
187
|
-
) -> MultiServerMCPClient:
|
|
189
|
+
) -> Tuple[MultiServerMCPClient, List[BaseTool]]:
|
|
188
190
|
"""
|
|
189
191
|
Creates and connects the MultiServerMCPClient using the token provided by
|
|
190
192
|
`access_token_provider`. Supports `streamable_http` and `stdio` transports.
|
|
193
|
+
|
|
194
|
+
Also returns the tools fetched per server during connection validation, so
|
|
195
|
+
callers don't have to pay for a second `get_tools()` round trip against
|
|
196
|
+
every server just to populate their toolkit (see mcp_runtime.py).
|
|
191
197
|
"""
|
|
192
198
|
|
|
193
199
|
for s in mcp_servers:
|
|
@@ -262,10 +268,13 @@ async def get_connected_mcp_client_for_agent(
|
|
|
262
268
|
connections, tool_interceptors=tool_interceptors or []
|
|
263
269
|
)
|
|
264
270
|
|
|
265
|
-
# Validate connections by attempting to load tools per server
|
|
271
|
+
# Validate connections by attempting to load tools per server. This is also
|
|
272
|
+
# the ONLY tool-fetch round trip we make per server — the fetched tools are
|
|
273
|
+
# returned to the caller so `MCPRuntime._run_lifecycle` doesn't need to call
|
|
274
|
+
# `client.get_tools()` again across all servers right after this returns.
|
|
266
275
|
exceptions: list[Exception] = []
|
|
267
276
|
failure_messages: list[str] = []
|
|
268
|
-
|
|
277
|
+
fetched_tools: list[BaseTool] = []
|
|
269
278
|
for server in mcp_servers:
|
|
270
279
|
conn_entry = connections.get(server.id) or {}
|
|
271
280
|
transport = conn_entry.get("transport", "unknown")
|
|
@@ -294,7 +303,7 @@ async def get_connected_mcp_client_for_agent(
|
|
|
294
303
|
len(tools),
|
|
295
304
|
dur_ms,
|
|
296
305
|
)
|
|
297
|
-
|
|
306
|
+
fetched_tools.extend(tools)
|
|
298
307
|
except Exception as e:
|
|
299
308
|
dur_ms = (time.perf_counter() - start) * 1000
|
|
300
309
|
logger.warning(
|
|
@@ -328,6 +337,6 @@ async def get_connected_mcp_client_for_agent(
|
|
|
328
337
|
logger.debug(
|
|
329
338
|
"[MCP][%s] summary: all servers validated, total tools=%d",
|
|
330
339
|
agent_id,
|
|
331
|
-
|
|
340
|
+
len(fetched_tools),
|
|
332
341
|
)
|
|
333
|
-
return client
|
|
342
|
+
return client, fetched_tools
|
|
@@ -38,7 +38,7 @@ from langchain_core.tools import BaseTool
|
|
|
38
38
|
from langgraph.types import Checkpointer
|
|
39
39
|
|
|
40
40
|
from fred_runtime.react.react_prompting import (
|
|
41
|
-
|
|
41
|
+
compose_system_prompt as _compose_system_prompt,
|
|
42
42
|
)
|
|
43
43
|
from fred_runtime.react.react_prompting import (
|
|
44
44
|
render_prompt_template as _render_prompt_template,
|
|
@@ -117,11 +117,17 @@ class DeepAgentRuntime(ReActRuntime):
|
|
|
117
117
|
binding=binding,
|
|
118
118
|
agent_id=self.definition.agent_id,
|
|
119
119
|
)
|
|
120
|
-
system_prompt = (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
system_prompt = _compose_system_prompt(
|
|
121
|
+
system_prompt,
|
|
122
|
+
binding=binding,
|
|
123
|
+
definition=self.definition,
|
|
124
|
+
agent_id=self.definition.agent_id,
|
|
125
|
+
tool_suffix=_build_runtime_tool_prompt_suffix(bound_tools),
|
|
126
|
+
runtime_suffixes=(
|
|
127
|
+
_filesystem_prompt_suffix(
|
|
128
|
+
filesystem_tools_enabled=filesystem_tools_enabled
|
|
129
|
+
),
|
|
130
|
+
),
|
|
125
131
|
)
|
|
126
132
|
compiled_agent = _create_compiled_deep_agent(
|
|
127
133
|
model=self._model,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright Thales 2026
|
|
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
|
+
Registry mapping catalog `provider` keys to in-process MCP toolkits (MIGR-03.03).
|
|
16
|
+
|
|
17
|
+
Why this module exists:
|
|
18
|
+
- an MCP catalog server with `transport: inprocess` names a `provider`; the MCP
|
|
19
|
+
runtime asks this factory to build the matching toolkit for the current agent turn
|
|
20
|
+
- keeping the mapping here (not in `agent_app`) lets the runtime wire a single
|
|
21
|
+
factory reference at startup without hardcoding provider knowledge in the boot path
|
|
22
|
+
|
|
23
|
+
How to use it:
|
|
24
|
+
- pass `build_inprocess_toolkit` as `RuntimeConfig.inprocess_toolkit_factory`
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from fred_runtime.integrations.kf_vector_search import (
|
|
33
|
+
KF_VECTOR_SEARCH_PROVIDER,
|
|
34
|
+
KfVectorSearchToolkit,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_inprocess_toolkit(provider: str | None, agent: Any) -> Any | None:
|
|
41
|
+
"""
|
|
42
|
+
Build the in-process toolkit for `provider`, bound to the current agent turn.
|
|
43
|
+
|
|
44
|
+
Returns None for an unknown or missing provider so the MCP runtime skips it with
|
|
45
|
+
a warning rather than failing the whole agent.
|
|
46
|
+
"""
|
|
47
|
+
if provider == KF_VECTOR_SEARCH_PROVIDER:
|
|
48
|
+
return KfVectorSearchToolkit(agent=agent)
|
|
49
|
+
logger.warning("[MCP] unknown inprocess provider=%s; no toolkit built.", provider)
|
|
50
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright Thales 2026
|
|
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
|
+
"""In-process Knowledge Flow vector-search MCP toolkit."""
|
|
15
|
+
|
|
16
|
+
from fred_runtime.integrations.kf_vector_search.toolkit import (
|
|
17
|
+
KF_VECTOR_SEARCH_PROVIDER,
|
|
18
|
+
KfVectorSearchToolkit,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = ["KF_VECTOR_SEARCH_PROVIDER", "KfVectorSearchToolkit"]
|