fred-runtime 3.3.1__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.1 → fred_runtime-3.3.2}/PKG-INFO +1 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/agent_app.py +50 -4
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/mcp_runtime.py +111 -17
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/mcp_utils.py +17 -8
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/graph/graph_runtime.py +0 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/v2_runtime/adapters.py +1 -2
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_runtime.py +0 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/PKG-INFO +1 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/pyproject.toml +1 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_agent_app.py +132 -3
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_react_thinking.py +1 -1
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/README.md +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/config.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/context.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/app/openai_compat_router.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/repl.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_logs_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/kf_workspace_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/deep/deep_runtime.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_prompting.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_context.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/support/tool_approval.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/support/tool_loop.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/SOURCES.txt +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/requires.txt +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/setup.cfg +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_config_loader.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_context.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_fred_workspace_fs.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_history.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_kf_vector_search_toolkit.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_kf_workspace_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_model_routing.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_react_prompting.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_runtime_context_prompt_injection.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_smoke.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_sql_checkpointer_owner.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_url_helpers.py +0 -0
- {fred_runtime-3.3.1 → fred_runtime-3.3.2}/tests/test_user_token_refresher.py +0 -0
|
@@ -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
|
|
@@ -1127,6 +1128,12 @@ async def _authorize_execution_or_raise(
|
|
|
1127
1128
|
- security disabled (no authenticated user) → skip (dev/local).
|
|
1128
1129
|
- ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
|
|
1129
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).
|
|
1130
1137
|
- otherwise require the caller to hold `CAN_READ` on the requested team — the
|
|
1131
1138
|
same relation the control-plane required before it would mint a grant. The
|
|
1132
1139
|
team is caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
@@ -1202,6 +1209,36 @@ async def _authorize_execution_or_raise(
|
|
|
1202
1209
|
agent_instance_id=request.agent_instance_id,
|
|
1203
1210
|
)
|
|
1204
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
|
+
)
|
|
1205
1242
|
try:
|
|
1206
1243
|
await rebac.check_user_team_permission_or_raise(
|
|
1207
1244
|
authenticated_user, TeamPermission.CAN_READ, team_id
|
|
@@ -2054,7 +2091,6 @@ async def _iterate_runtime_event_payloads(
|
|
|
2054
2091
|
checkpoint_id=resolved_checkpoint_id,
|
|
2055
2092
|
user_id=ctx.get("user_id"),
|
|
2056
2093
|
team_id=resolved_team_id,
|
|
2057
|
-
user_groups=ctx.get("user_groups"),
|
|
2058
2094
|
language=ctx.get("language"),
|
|
2059
2095
|
access_token=access_token,
|
|
2060
2096
|
refresh_token=ctx.get("refresh_token"),
|
|
@@ -2258,11 +2294,19 @@ def _build_agent_router(
|
|
|
2258
2294
|
- developers need to validate KPI emission from the CLI without
|
|
2259
2295
|
Grafana or Prometheus; this exposes the pod-local ring buffer
|
|
2260
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.
|
|
2261
2305
|
"""
|
|
2262
2306
|
rebac = get_runtime_context().config.rebac_engine
|
|
2263
2307
|
if caller is not None and rebac is not None and rebac.enabled:
|
|
2264
2308
|
await rebac.check_user_permission_or_raise(
|
|
2265
|
-
caller, OrganizationPermission.
|
|
2309
|
+
caller, OrganizationPermission.CAN_MANAGE_PLATFORM, ORGANIZATION_ID
|
|
2266
2310
|
)
|
|
2267
2311
|
with container._kpi_turns_lock:
|
|
2268
2312
|
events = list(container.kpi_turns_buffer)
|
|
@@ -2991,8 +3035,10 @@ def _build_agent_router(
|
|
|
2991
3035
|
stream closes. Do not rely on it as an end-of-turn signal here.
|
|
2992
3036
|
|
|
2993
3037
|
Security:
|
|
2994
|
-
-
|
|
2995
|
-
|
|
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.
|
|
2996
3042
|
- RBAC via Keycloak and REBAC via OpenFGA protect this endpoint.
|
|
2997
3043
|
|
|
2998
3044
|
Architectural note:
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -1903,11 +1903,10 @@ class KPIWriterMetricsAdapter(MetricsProvider):
|
|
|
1903
1903
|
name: str,
|
|
1904
1904
|
*,
|
|
1905
1905
|
dims: dict[str, str | None] | None = None,
|
|
1906
|
-
groups: list[str] | None = None,
|
|
1907
1906
|
) -> Generator[dict[str, str | None], None, None]:
|
|
1908
1907
|
with self._kpi.timer(
|
|
1909
1908
|
name,
|
|
1910
1909
|
dims=dims,
|
|
1911
|
-
actor=KPIActor(type="system"
|
|
1910
|
+
actor=KPIActor(type="system"),
|
|
1912
1911
|
) as recorded_dims:
|
|
1913
1912
|
yield recorded_dims
|
|
@@ -322,7 +322,6 @@ class _TransportBackedReActExecutor(Executor[ReActInput, ReActOutput]):
|
|
|
322
322
|
"agent_instance_id": baggage.get("agent_instance_id"),
|
|
323
323
|
"template_agent_id": baggage.get("template_agent_id"),
|
|
324
324
|
},
|
|
325
|
-
groups=self._binding.runtime_context.user_groups,
|
|
326
325
|
)
|
|
327
326
|
else:
|
|
328
327
|
phase_timer_ctx = nullcontext()
|
|
@@ -29,8 +29,10 @@ from typing import cast
|
|
|
29
29
|
|
|
30
30
|
import pytest
|
|
31
31
|
from conftest import StaticChatModelFactory, ToolFriendlyFakeChatModel
|
|
32
|
+
from fastapi.routing import APIRoute
|
|
32
33
|
from fastapi.testclient import TestClient
|
|
33
34
|
from fred_core.common.config_loader import get_config
|
|
35
|
+
from fred_core.common.team_id import personal_team_id
|
|
34
36
|
from fred_core.kpi.kpi_writer import KPIWriter
|
|
35
37
|
from fred_core.kpi.log_kpi_store import KpiLogStore
|
|
36
38
|
from fred_core.kpi.prometheus_kpi_store import PrometheusKPIStore
|
|
@@ -1693,7 +1695,7 @@ def _wire_engine(
|
|
|
1693
1695
|
)
|
|
1694
1696
|
|
|
1695
1697
|
|
|
1696
|
-
_ALICE = KeycloakUser(uid="alice", username="alice", roles=[], email=None
|
|
1698
|
+
_ALICE = KeycloakUser(uid="alice", username="alice", roles=[], email=None)
|
|
1697
1699
|
|
|
1698
1700
|
|
|
1699
1701
|
@pytest.mark.asyncio
|
|
@@ -1824,7 +1826,6 @@ _WORKER = KeycloakUser(
|
|
|
1824
1826
|
username="service-account-fred-evaluation-worker",
|
|
1825
1827
|
roles=["service_agent"],
|
|
1826
1828
|
email=None,
|
|
1827
|
-
groups=[],
|
|
1828
1829
|
)
|
|
1829
1830
|
|
|
1830
1831
|
|
|
@@ -1869,6 +1870,72 @@ async def test_authorize_service_agent_still_requires_team(
|
|
|
1869
1870
|
assert engine.calls == []
|
|
1870
1871
|
|
|
1871
1872
|
|
|
1873
|
+
_BOB = KeycloakUser(uid="bob", username="bob", roles=[], email=None)
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
@pytest.mark.asyncio
|
|
1877
|
+
async def test_authorize_allows_personal_space_owner_without_openfga(
|
|
1878
|
+
monkeypatch, minimal_config
|
|
1879
|
+
) -> None:
|
|
1880
|
+
"""A human caller acting on their own canonical personal_team_id is authorized
|
|
1881
|
+
as intrinsic ownership — no OpenFGA call (AUTHZ-05 item 8b watch item)."""
|
|
1882
|
+
engine = _FakeRebacEngine(enabled=True, deny=True) # would deny if consulted
|
|
1883
|
+
_wire_engine(monkeypatch, engine)
|
|
1884
|
+
container = PodApplicationContext(minimal_config)
|
|
1885
|
+
|
|
1886
|
+
await agent_app_module._authorize_execution_or_raise(
|
|
1887
|
+
_managed_request(team_id=personal_team_id(_ALICE.uid)), _ALICE, container
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1890
|
+
assert engine.calls == []
|
|
1891
|
+
with container._audit_events_lock:
|
|
1892
|
+
events = list(container.audit_events_buffer)
|
|
1893
|
+
assert events[-1]["audit_event"] == "personal_space_owner_authorized"
|
|
1894
|
+
assert events[-1].get("team_id") == personal_team_id(_ALICE.uid)
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
@pytest.mark.asyncio
|
|
1898
|
+
async def test_authorize_denies_other_users_personal_space(
|
|
1899
|
+
monkeypatch, minimal_config
|
|
1900
|
+
) -> None:
|
|
1901
|
+
"""Alice requesting Bob's personal space is denied outright — a permissive
|
|
1902
|
+
(or residual) OpenFGA tuple must never be able to rescue this."""
|
|
1903
|
+
engine = _FakeRebacEngine(enabled=True, deny=False) # would allow if consulted
|
|
1904
|
+
_wire_engine(monkeypatch, engine)
|
|
1905
|
+
container = PodApplicationContext(minimal_config)
|
|
1906
|
+
|
|
1907
|
+
with pytest.raises(agent_app_module.HTTPException) as exc:
|
|
1908
|
+
await agent_app_module._authorize_execution_or_raise(
|
|
1909
|
+
_managed_request(team_id=personal_team_id(_BOB.uid)), _ALICE, container
|
|
1910
|
+
)
|
|
1911
|
+
|
|
1912
|
+
assert exc.value.status_code == 403
|
|
1913
|
+
assert engine.calls == []
|
|
1914
|
+
with container._audit_events_lock:
|
|
1915
|
+
events = list(container.audit_events_buffer)
|
|
1916
|
+
assert events[-1]["audit_event"] == "personal_space_denied"
|
|
1917
|
+
assert events[-1].get("user_id") == "alice"
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
@pytest.mark.asyncio
|
|
1921
|
+
async def test_authorize_denies_ambiguous_personal_alias(
|
|
1922
|
+
monkeypatch, minimal_config
|
|
1923
|
+
) -> None:
|
|
1924
|
+
"""The bare "personal" alias is ambiguous once ReBAC is active — reject it
|
|
1925
|
+
rather than resolving it as if it meant the caller's own space."""
|
|
1926
|
+
engine = _FakeRebacEngine(enabled=True, deny=False)
|
|
1927
|
+
_wire_engine(monkeypatch, engine)
|
|
1928
|
+
container = PodApplicationContext(minimal_config)
|
|
1929
|
+
|
|
1930
|
+
with pytest.raises(agent_app_module.HTTPException) as exc:
|
|
1931
|
+
await agent_app_module._authorize_execution_or_raise(
|
|
1932
|
+
_managed_request(team_id="personal"), _ALICE, container
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
assert exc.value.status_code == 403
|
|
1936
|
+
assert engine.calls == []
|
|
1937
|
+
|
|
1938
|
+
|
|
1872
1939
|
# ---------------------------------------------------------------------------
|
|
1873
1940
|
# RUNTIME-07 rev. 2 (F-B / F-C) — JWT identity + private-per-owner sessions
|
|
1874
1941
|
# ---------------------------------------------------------------------------
|
|
@@ -1964,7 +2031,9 @@ async def test_session_ownership_skipped_when_security_disabled(
|
|
|
1964
2031
|
|
|
1965
2032
|
|
|
1966
2033
|
class _FakePlatformRebacEngine:
|
|
1967
|
-
"""RebacEngine stand-in exposing has_user_permission for the C1 admin branch
|
|
2034
|
+
"""RebacEngine stand-in exposing has_user_permission for the C1 admin branch,
|
|
2035
|
+
plus check_user_permission_or_raise for the /kpi-turns and /audit-events
|
|
2036
|
+
ring-buffer endpoints' CAN_MANAGE_PLATFORM gate."""
|
|
1968
2037
|
|
|
1969
2038
|
def __init__(self, *, enabled: bool, grant: bool) -> None:
|
|
1970
2039
|
self._enabled = enabled
|
|
@@ -1981,6 +2050,13 @@ class _FakePlatformRebacEngine:
|
|
|
1981
2050
|
self.calls.append((user.uid, permission, resource_id))
|
|
1982
2051
|
return self._grant
|
|
1983
2052
|
|
|
2053
|
+
async def check_user_permission_or_raise(
|
|
2054
|
+
self, user: KeycloakUser, permission: object, resource_id: str, **_kw: object
|
|
2055
|
+
) -> None:
|
|
2056
|
+
self.calls.append((user.uid, permission, resource_id))
|
|
2057
|
+
if not self._grant:
|
|
2058
|
+
raise AuthorizationError(user.uid, str(permission), Resource.RESOURCES)
|
|
2059
|
+
|
|
1984
2060
|
|
|
1985
2061
|
@pytest.mark.asyncio
|
|
1986
2062
|
async def test_caller_can_manage_platform_true_when_enabled_and_granted(
|
|
@@ -2027,6 +2103,59 @@ async def test_caller_can_manage_platform_false_when_no_caller(monkeypatch) -> N
|
|
|
2027
2103
|
assert engine.calls == []
|
|
2028
2104
|
|
|
2029
2105
|
|
|
2106
|
+
def _get_kpi_turns_endpoint():
|
|
2107
|
+
"""Grab the `/kpi-turns` route's raw endpoint function, bypassing FastAPI's
|
|
2108
|
+
dependency-injection layer so it can be called directly with explicit args."""
|
|
2109
|
+
router = agent_app_module._build_agent_router(registry={}, security_enabled=True)
|
|
2110
|
+
return next(
|
|
2111
|
+
route.endpoint
|
|
2112
|
+
for route in router.routes
|
|
2113
|
+
if isinstance(route, APIRoute) and route.endpoint.__name__ == "get_kpi_turns"
|
|
2114
|
+
)
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
@pytest.mark.asyncio
|
|
2118
|
+
async def test_get_kpi_turns_requires_can_manage_platform_when_enforcing(
|
|
2119
|
+
monkeypatch, minimal_config
|
|
2120
|
+
) -> None:
|
|
2121
|
+
"""AUTHZ-05 review finding: item 8a's blanket removal of the org-level
|
|
2122
|
+
CAN_READ_METRICS check does not apply to this ring buffer — it exposes
|
|
2123
|
+
cross-user/cross-team session_id/user_id/team_id/token data for every
|
|
2124
|
+
caller that has hit this pod, unlike the tier-2 capabilities item 8a
|
|
2125
|
+
correctly deleted elsewhere. Gated on CAN_MANAGE_PLATFORM, same as the
|
|
2126
|
+
sibling `get_audit_events`, when ReBAC is actually enforcing."""
|
|
2127
|
+
endpoint = _get_kpi_turns_endpoint()
|
|
2128
|
+
container = PodApplicationContext(minimal_config)
|
|
2129
|
+
|
|
2130
|
+
denying_engine = _FakePlatformRebacEngine(enabled=True, grant=False)
|
|
2131
|
+
_wire_engine(monkeypatch, denying_engine)
|
|
2132
|
+
with pytest.raises(AuthorizationError):
|
|
2133
|
+
await endpoint(limit=10, container=container, caller=_ALICE)
|
|
2134
|
+
assert denying_engine.calls == [
|
|
2135
|
+
("alice", OrganizationPermission.CAN_MANAGE_PLATFORM, ORGANIZATION_ID)
|
|
2136
|
+
]
|
|
2137
|
+
|
|
2138
|
+
granting_engine = _FakePlatformRebacEngine(enabled=True, grant=True)
|
|
2139
|
+
_wire_engine(monkeypatch, granting_engine)
|
|
2140
|
+
assert await endpoint(limit=10, container=container, caller=_ALICE) == []
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
@pytest.mark.asyncio
|
|
2144
|
+
async def test_get_kpi_turns_unchanged_in_dev_mode(monkeypatch, minimal_config) -> None:
|
|
2145
|
+
"""Dev/no-security mode (no caller, or ReBAC disabled) stays exactly as
|
|
2146
|
+
before this fix: the diagnostic buffer remains reachable without a grant."""
|
|
2147
|
+
endpoint = _get_kpi_turns_endpoint()
|
|
2148
|
+
container = PodApplicationContext(minimal_config)
|
|
2149
|
+
|
|
2150
|
+
_wire_engine(monkeypatch, None)
|
|
2151
|
+
assert await endpoint(limit=10, container=container, caller=None) == []
|
|
2152
|
+
|
|
2153
|
+
disabled_engine = _FakePlatformRebacEngine(enabled=False, grant=False)
|
|
2154
|
+
_wire_engine(monkeypatch, disabled_engine)
|
|
2155
|
+
assert await endpoint(limit=10, container=container, caller=_ALICE) == []
|
|
2156
|
+
assert disabled_engine.calls == [] # a disabled engine is never consulted
|
|
2157
|
+
|
|
2158
|
+
|
|
2030
2159
|
@pytest.mark.asyncio
|
|
2031
2160
|
async def test_identity_is_stamped_from_jwt_and_body_tokens_neutralized(
|
|
2032
2161
|
monkeypatch, minimal_config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/inprocess_toolkit_registry.py
RENAMED
|
File without changes
|
{fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/kf_vector_search/__init__.py
RENAMED
|
File without changes
|
{fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/integrations/kf_vector_search/toolkit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/request_context_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{fred_runtime-3.3.1 → fred_runtime-3.3.2}/fred_runtime/runtime_support/user_token_refresher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|