fred-runtime 3.1.0__tar.gz → 3.3.0__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.1.0 → fred_runtime-3.3.0}/PKG-INFO +3 -3
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/agent_app.py +280 -156
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/config.py +9 -5
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/openai_compat_router.py +52 -7
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/repl.py +1 -1
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/integrations/v2_runtime/adapters.py +78 -7
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_context.py +9 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/PKG-INFO +3 -3
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/requires.txt +2 -2
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/pyproject.toml +3 -3
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_agent_app.py +529 -76
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_fred_workspace_fs.py +98 -14
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_kf_workspace_client.py +3 -1
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/README.md +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/_catalogs.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/config_loader.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/container.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/context.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/dependencies.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/mcp_config.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/app/observability_factory.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/completion.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/entrypoint.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/history_display.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/kpi_display.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/pod_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/repl_helpers.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/cli/url_helpers.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/context_aware_tool.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_base_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_http_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_logs_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/kf_workspace_client.py +1 -1
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/mcp_interceptors.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/mcp_runtime.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/mcp_toolkit.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/mcp_utils.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/structures.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/token_expiry.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/common/tool_node_utils.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/deep/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/deep/deep_runtime.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/eval/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/eval/collector.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/graph/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/graph/graph_runtime.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/integrations/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/model_routing/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/model_routing/catalog.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/model_routing/contracts.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/model_routing/provider.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/model_routing/resolver.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_message_codec.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_model_adapter.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_prompting.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_runtime.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_stream_adapter.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tool_binding.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tool_loop.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tool_rendering.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tool_resolution.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tool_utils.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/react/react_tracing.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/support/__init__.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/support/filesystem_context.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/support/thinking.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/support/tool_approval.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime/support/tool_loop.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/SOURCES.txt +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/entry_points.txt +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/fred_runtime.egg-info/top_level.txt +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/setup.cfg +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_config_loader.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_context.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_context_aware_tool.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_conversational_memory.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_eval_collector.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_eval_trace.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_graph_runtime_observability.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_history.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_kpi_display.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_mcp_config.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_model_routing.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_openai_compat_router.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_pod_client.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_react_thinking.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_repl_helpers.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_smoke.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_token_expiry.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_url_helpers.py +0 -0
- {fred_runtime-3.1.0 → fred_runtime-3.3.0}/tests/test_user_token_refresher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred-runtime
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
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.
|
|
16
|
-
Requires-Dist: fred-sdk>=3.
|
|
15
|
+
Requires-Dist: fred-core>=3.4.0
|
|
16
|
+
Requires-Dist: fred-sdk>=3.3.0
|
|
17
17
|
Requires-Dist: alembic>=1.18.4
|
|
18
18
|
Requires-Dist: deepagents>=0.4.11
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -61,7 +61,10 @@ from fred_core.kpi import KPIMiddleware
|
|
|
61
61
|
from fred_core.kpi.kpi_writer_structures import KPIActor
|
|
62
62
|
from fred_core.logs.log_setup import log_setup
|
|
63
63
|
from fred_core.logs.memory_log_store import RamLogStore
|
|
64
|
+
from fred_core.security.models import AuthorizationError
|
|
64
65
|
from fred_core.security.oidc import get_keycloak_client_id, get_keycloak_url
|
|
66
|
+
from fred_core.security.rebac.rebac_engine import TeamPermission
|
|
67
|
+
from fred_core.security.rebac.rebac_factory import rebac_factory
|
|
65
68
|
from fred_core.security.structure import KeycloakUser
|
|
66
69
|
from fred_sdk.contracts.context import (
|
|
67
70
|
AgentInvocationRequest,
|
|
@@ -75,9 +78,7 @@ from fred_sdk.contracts.context import (
|
|
|
75
78
|
from fred_sdk.contracts.eval import EvalStep, EvalTrace
|
|
76
79
|
from fred_sdk.contracts.execution import (
|
|
77
80
|
ExecutionGrantAction,
|
|
78
|
-
ExecutionGrantViolation,
|
|
79
81
|
RuntimeExecuteRequest,
|
|
80
|
-
validate_execution_grant,
|
|
81
82
|
)
|
|
82
83
|
from fred_sdk.contracts.models import (
|
|
83
84
|
AgentTuning,
|
|
@@ -961,6 +962,7 @@ async def _resolve_agent_instance(
|
|
|
961
962
|
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
962
963
|
access_token: str | None,
|
|
963
964
|
control_plane_url: str | None,
|
|
965
|
+
team_id: str | None = None,
|
|
964
966
|
) -> _ResolvedExecutionTarget:
|
|
965
967
|
"""
|
|
966
968
|
Resolve a direct or managed execution target into a concrete definition.
|
|
@@ -979,11 +981,15 @@ async def _resolve_agent_instance(
|
|
|
979
981
|
|
|
980
982
|
if request.agent_id is not None:
|
|
981
983
|
definition = registry.get(request.agent_id)
|
|
982
|
-
|
|
984
|
+
# Direct agent_id execution takes no grant, so it is the enforcement point
|
|
985
|
+
# for agent visibility: a non-public agent (AgentDefinition.public=False) is
|
|
986
|
+
# internal — it may only be executed through a managed instance (whose
|
|
987
|
+
# enrollment is admin-gated), never directly by id. Treat it as unknown so
|
|
988
|
+
# its existence is not even confirmed. See AGENT-VISIBILITY-RFC §3.1.
|
|
989
|
+
if definition is None or not getattr(definition, "public", True):
|
|
983
990
|
raise HTTPException(
|
|
984
991
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
985
|
-
detail=f"Unknown agent_id: {request.agent_id!r}.
|
|
986
|
-
f"Known agents: {list(registry.keys())}",
|
|
992
|
+
detail=f"Unknown agent_id: {request.agent_id!r}.",
|
|
987
993
|
)
|
|
988
994
|
if request.inline_tuning:
|
|
989
995
|
available_mcp_servers = _available_mcp_servers_for_definition(definition)
|
|
@@ -1012,8 +1018,20 @@ async def _resolve_agent_instance(
|
|
|
1012
1018
|
),
|
|
1013
1019
|
)
|
|
1014
1020
|
|
|
1021
|
+
# Team-scoped resolution (RUNTIME-07 rev. 2). The pod resolves the instance's
|
|
1022
|
+
# template + tuning from the control-plane binding scoped to the caller's team
|
|
1023
|
+
# (ReBAC-gated, store.get_for_team) — the replacement for the signed grant.
|
|
1024
|
+
# The end user has already been authorized at this pod (Keycloak + OpenFGA).
|
|
1025
|
+
if team_id is None:
|
|
1026
|
+
raise HTTPException(
|
|
1027
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1028
|
+
detail=(
|
|
1029
|
+
"Managed agent instance execution requires a team context "
|
|
1030
|
+
"(runtime_context.team_id)."
|
|
1031
|
+
),
|
|
1032
|
+
)
|
|
1015
1033
|
url = (
|
|
1016
|
-
f"{control_plane_url.rstrip('/')}/agent-instances/"
|
|
1034
|
+
f"{control_plane_url.rstrip('/')}/teams/{team_id}/agent-instances/"
|
|
1017
1035
|
f"{request.agent_instance_id}/runtime"
|
|
1018
1036
|
)
|
|
1019
1037
|
headers = {"Authorization": f"Bearer {access_token}"} if access_token else None
|
|
@@ -1086,84 +1104,239 @@ def _make_user_dependency(
|
|
|
1086
1104
|
return _dep_noop
|
|
1087
1105
|
|
|
1088
1106
|
|
|
1089
|
-
def
|
|
1107
|
+
async def _authorize_execution_or_raise(
|
|
1090
1108
|
request: RuntimeExecuteRequest,
|
|
1091
1109
|
authenticated_user: KeycloakUser | None,
|
|
1092
1110
|
container: PodApplicationContext,
|
|
1093
1111
|
) -> None:
|
|
1094
1112
|
"""
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
-
|
|
1106
|
-
|
|
1107
|
-
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1113
|
+
Pod-side OpenFGA authorization for one execution request (RUNTIME-07 rev. 2).
|
|
1114
|
+
|
|
1115
|
+
The pod is the execution authority. Identity is proven by the Keycloak JWT
|
|
1116
|
+
(`authenticated_user`); authorization is decided HERE, per request, by an
|
|
1117
|
+
OpenFGA check on the team the caller is acting in. This is the model already
|
|
1118
|
+
homologated on `main` (agentic-backend), re-instantiated per pod — it replaces
|
|
1119
|
+
the control-plane-signed grant, which is being removed.
|
|
1120
|
+
|
|
1121
|
+
Behaviour:
|
|
1122
|
+
- security disabled (no authenticated user) → skip (dev/local).
|
|
1123
|
+
- ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
|
|
1124
|
+
the C3 profile guarantees an enabled engine in classified deployments.
|
|
1125
|
+
- otherwise require the caller to hold `CAN_READ` on the requested team — the
|
|
1126
|
+
same relation the control-plane required before it would mint a grant. The
|
|
1127
|
+
team is caller-supplied but safe: OpenFGA only authorizes teams the user
|
|
1128
|
+
actually holds a relation to. Authorization and denial are both audited;
|
|
1129
|
+
any OpenFGA denial fails closed (403).
|
|
1130
|
+
|
|
1131
|
+
Covers execute, execute/stream, evaluate AND resume — every path funnels
|
|
1132
|
+
through this call, so no half-authenticated session is possible.
|
|
1133
|
+
|
|
1134
|
+
Direct template execution (`agent_id`) is **forbidden under the c3 profile**
|
|
1135
|
+
(RUNTIME-07 F-D); in dev/non-c3 it stays identity-only. Managed execution
|
|
1136
|
+
(`agent_instance_id`) requires a team and an OpenFGA grant whenever ReBAC is
|
|
1137
|
+
active; a missing team then fails closed.
|
|
1111
1138
|
"""
|
|
1112
1139
|
if authenticated_user is None:
|
|
1113
|
-
# Security disabled (dev mode) —
|
|
1140
|
+
# Security disabled (dev mode) — no identity to authorize.
|
|
1114
1141
|
return
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1142
|
+
profile = getattr(get_runtime_context().config, "security_profile", None)
|
|
1143
|
+
|
|
1144
|
+
# Direct template execution (agent_id): no team scope, no managed instance.
|
|
1145
|
+
if request.agent_id is not None:
|
|
1146
|
+
if profile == "c3":
|
|
1147
|
+
_emit_audit_event(
|
|
1148
|
+
container,
|
|
1149
|
+
"warning",
|
|
1150
|
+
"direct_execution_forbidden",
|
|
1151
|
+
user_id=authenticated_user.uid,
|
|
1152
|
+
agent_id=request.agent_id,
|
|
1153
|
+
)
|
|
1154
|
+
raise HTTPException(
|
|
1155
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1156
|
+
detail=(
|
|
1157
|
+
"direct agent_id execution is not permitted under the c3 "
|
|
1158
|
+
"security profile; use a managed agent instance"
|
|
1159
|
+
),
|
|
1160
|
+
)
|
|
1161
|
+
# dev / non-c3: identity-only (no team to authorize against).
|
|
1118
1162
|
return
|
|
1119
|
-
|
|
1163
|
+
|
|
1164
|
+
# Managed execution (agent_instance_id).
|
|
1165
|
+
rebac = get_runtime_context().config.rebac_engine
|
|
1166
|
+
if rebac is None or not rebac.enabled:
|
|
1167
|
+
# ReBAC not active (Noop / unconfigured) — identity-only dev posture. The
|
|
1168
|
+
# c3 profile guarantees an enabled engine in production (fail-closed).
|
|
1169
|
+
return
|
|
1170
|
+
team_id = request.effective_team_id()
|
|
1171
|
+
if team_id is None:
|
|
1120
1172
|
_emit_audit_event(
|
|
1121
1173
|
container,
|
|
1122
1174
|
"warning",
|
|
1123
|
-
"
|
|
1124
|
-
|
|
1125
|
-
|
|
1175
|
+
"managed_execution_without_team",
|
|
1176
|
+
user_id=authenticated_user.uid,
|
|
1177
|
+
agent_instance_id=request.agent_instance_id,
|
|
1126
1178
|
)
|
|
1127
1179
|
raise HTTPException(
|
|
1128
1180
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
1129
1181
|
detail=(
|
|
1130
|
-
|
|
1131
|
-
|
|
1182
|
+
"managed agent instance execution requires a team context "
|
|
1183
|
+
"(runtime_context.team_id)"
|
|
1132
1184
|
),
|
|
1133
1185
|
)
|
|
1186
|
+
try:
|
|
1187
|
+
await rebac.check_user_team_permission_or_raise(
|
|
1188
|
+
authenticated_user, TeamPermission.CAN_READ, team_id
|
|
1189
|
+
)
|
|
1190
|
+
except AuthorizationError as exc:
|
|
1191
|
+
_emit_audit_event(
|
|
1192
|
+
container,
|
|
1193
|
+
"warning",
|
|
1194
|
+
"rebac_denied",
|
|
1195
|
+
user_id=authenticated_user.uid,
|
|
1196
|
+
team_id=team_id,
|
|
1197
|
+
agent_instance_id=request.agent_instance_id,
|
|
1198
|
+
reason=str(exc),
|
|
1199
|
+
)
|
|
1200
|
+
raise HTTPException(
|
|
1201
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1202
|
+
detail=(
|
|
1203
|
+
f"user {authenticated_user.uid!r} is not authorized for "
|
|
1204
|
+
f"team {team_id!r}"
|
|
1205
|
+
),
|
|
1206
|
+
) from exc
|
|
1134
1207
|
_emit_audit_event(
|
|
1135
1208
|
container,
|
|
1136
1209
|
"info",
|
|
1137
|
-
"
|
|
1210
|
+
"rebac_authorized",
|
|
1138
1211
|
user_id=authenticated_user.uid,
|
|
1212
|
+
team_id=team_id,
|
|
1139
1213
|
agent_instance_id=request.agent_instance_id,
|
|
1140
1214
|
)
|
|
1141
1215
|
|
|
1142
1216
|
|
|
1143
|
-
def
|
|
1217
|
+
async def _enforce_session_ownership(
|
|
1144
1218
|
request: RuntimeExecuteRequest,
|
|
1145
|
-
|
|
1219
|
+
authenticated_user: KeycloakUser | None,
|
|
1220
|
+
container: PodApplicationContext,
|
|
1221
|
+
) -> None:
|
|
1146
1222
|
"""
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
-
|
|
1223
|
+
Private-per-owner session policy (RUNTIME-07 rev. 2, finding F-C).
|
|
1224
|
+
|
|
1225
|
+
Conversations are private to their owner. When security is enabled and the
|
|
1226
|
+
request targets an EXISTING session, the authenticated user must own it. A
|
|
1227
|
+
brand-new session is allowed (the caller becomes its owner). This blocks a
|
|
1228
|
+
same-team user from continuing or resuming another user's private session by
|
|
1229
|
+
guessing its `session_id` / `checkpoint_id` — the team OpenFGA check alone
|
|
1230
|
+
would not catch an intra-team cross-user access.
|
|
1231
|
+
"""
|
|
1232
|
+
if authenticated_user is None:
|
|
1233
|
+
return # security disabled (dev) — no identity to enforce
|
|
1234
|
+
session_id = request.effective_session_id()
|
|
1235
|
+
if not session_id:
|
|
1236
|
+
return
|
|
1237
|
+
history_store = get_runtime_context().config.history_store
|
|
1238
|
+
if history_store is None:
|
|
1239
|
+
return
|
|
1240
|
+
if not await history_store.session_exists(session_id):
|
|
1241
|
+
return # new session — the caller becomes its owner
|
|
1242
|
+
if await history_store.session_belongs_to_user(session_id, authenticated_user.uid):
|
|
1243
|
+
return # caller owns this session
|
|
1244
|
+
_emit_audit_event(
|
|
1245
|
+
container,
|
|
1246
|
+
"warning",
|
|
1247
|
+
"session_owner_mismatch",
|
|
1248
|
+
user_id=authenticated_user.uid,
|
|
1249
|
+
session_id=session_id,
|
|
1250
|
+
agent_instance_id=request.agent_instance_id,
|
|
1251
|
+
)
|
|
1252
|
+
raise HTTPException(
|
|
1253
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1254
|
+
detail=f"session {session_id!r} does not belong to the authenticated user",
|
|
1255
|
+
)
|
|
1153
1256
|
|
|
1154
|
-
How to use it:
|
|
1155
|
-
- call immediately before `validate_execution_grant(...)`
|
|
1156
|
-
- pass the returned action as `expected_action`
|
|
1157
1257
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1258
|
+
async def _authorize_and_resolve(
|
|
1259
|
+
request: RuntimeExecuteRequest,
|
|
1260
|
+
*,
|
|
1261
|
+
authenticated_user: KeycloakUser | None,
|
|
1262
|
+
container: PodApplicationContext,
|
|
1263
|
+
registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
|
|
1264
|
+
access_token: str | None,
|
|
1265
|
+
) -> tuple["_AgentExecuteRequest", _ResolvedExecutionTarget]:
|
|
1160
1266
|
"""
|
|
1267
|
+
Shared pre-execution gate for execute / execute-stream / evaluate (and HITL
|
|
1268
|
+
resume, which is a field on those endpoints) — RUNTIME-07 rev. 2.
|
|
1161
1269
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1270
|
+
The pod is the execution authority: there is NO control-plane-signed grant.
|
|
1271
|
+
1. validate checkpoint/session access,
|
|
1272
|
+
2. authorize the caller against OpenFGA on their team (identity = Keycloak JWT),
|
|
1273
|
+
3. resolve the managed instance template + tuning from the control-plane,
|
|
1274
|
+
team-scoped and ReBAC-gated (config only — never a secret or capability),
|
|
1275
|
+
4. cross-check the resolved owner team against the caller's claimed team.
|
|
1276
|
+
|
|
1277
|
+
Returns the internal request plus the resolved execution target.
|
|
1278
|
+
"""
|
|
1279
|
+
# F-B: identity is the validated Keycloak JWT, never the request body. Stamp
|
|
1280
|
+
# user_id from the token and neutralize any body-supplied credentials — the pod
|
|
1281
|
+
# uses the header bearer for downstream (knowledge-flow) calls and trusts no
|
|
1282
|
+
# caller-provided user_id / access_token / refresh_token.
|
|
1283
|
+
if authenticated_user is not None:
|
|
1284
|
+
base_ctx = request.runtime_context or RuntimeContext()
|
|
1285
|
+
request.runtime_context = base_ctx.model_copy(
|
|
1286
|
+
# F-B: neutralize body-supplied tokens (not secrets — set to None).
|
|
1287
|
+
update={
|
|
1288
|
+
"user_id": authenticated_user.uid,
|
|
1289
|
+
"access_token": access_token,
|
|
1290
|
+
"refresh_token": None, # nosec B105
|
|
1291
|
+
"access_token_expires_at": None, # nosec B105
|
|
1292
|
+
}
|
|
1293
|
+
)
|
|
1294
|
+
await _validate_session_checkpoint_access(request)
|
|
1295
|
+
await _enforce_session_ownership(request, authenticated_user, container)
|
|
1296
|
+
await _authorize_execution_or_raise(request, authenticated_user, container)
|
|
1297
|
+
internal_req = _to_internal_request(request)
|
|
1298
|
+
target = await _resolve_agent_instance(
|
|
1299
|
+
request=internal_req,
|
|
1300
|
+
registry=registry,
|
|
1301
|
+
access_token=access_token,
|
|
1302
|
+
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
1303
|
+
team_id=request.effective_team_id(),
|
|
1166
1304
|
)
|
|
1305
|
+
_validate_resolved_team(request, target.team_id, container)
|
|
1306
|
+
return internal_req, target
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def _validate_resolved_team(
|
|
1310
|
+
request: RuntimeExecuteRequest,
|
|
1311
|
+
resolved_team_id: str | None,
|
|
1312
|
+
container: PodApplicationContext,
|
|
1313
|
+
) -> None:
|
|
1314
|
+
"""
|
|
1315
|
+
Cross-check the resolved instance owner team against the caller's claim.
|
|
1316
|
+
|
|
1317
|
+
Team-scoped resolution already restricts the lookup to the caller's team, so a
|
|
1318
|
+
mismatch should be impossible; this is defense-in-depth and an audit anchor.
|
|
1319
|
+
Skipped for direct template execution (no team scope).
|
|
1320
|
+
"""
|
|
1321
|
+
if resolved_team_id is None:
|
|
1322
|
+
return
|
|
1323
|
+
claimed = request.effective_team_id()
|
|
1324
|
+
if claimed is not None and claimed != resolved_team_id:
|
|
1325
|
+
_emit_audit_event(
|
|
1326
|
+
container,
|
|
1327
|
+
"warning",
|
|
1328
|
+
"team_binding_mismatch",
|
|
1329
|
+
claimed_team_id=claimed,
|
|
1330
|
+
resolved_team_id=resolved_team_id,
|
|
1331
|
+
agent_instance_id=request.agent_instance_id,
|
|
1332
|
+
)
|
|
1333
|
+
raise HTTPException(
|
|
1334
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1335
|
+
detail=(
|
|
1336
|
+
f"resolved owner team {resolved_team_id!r} does not match "
|
|
1337
|
+
f"requested team {claimed!r}"
|
|
1338
|
+
),
|
|
1339
|
+
)
|
|
1167
1340
|
|
|
1168
1341
|
|
|
1169
1342
|
async def _validate_session_checkpoint_access(
|
|
@@ -1858,6 +2031,11 @@ async def _iterate_runtime_event_payloads(
|
|
|
1858
2031
|
include_session_scope=ctx.get("include_session_scope"),
|
|
1859
2032
|
include_corpus_scope=ctx.get("include_corpus_scope"),
|
|
1860
2033
|
deep_search=ctx.get("deep_search"),
|
|
2034
|
+
# The marketplace/library prompt selected for the conversation. The
|
|
2035
|
+
# control-plane resolves the session's attached prompts into this scalar
|
|
2036
|
+
# at prepare-execution and the frontend forwards it — but it was also
|
|
2037
|
+
# silently dropped here, so no agent ever received a selected prompt.
|
|
2038
|
+
context_prompt_text=ctx.get("context_prompt_text"),
|
|
1861
2039
|
)
|
|
1862
2040
|
|
|
1863
2041
|
binding = BoundRuntimeContext(
|
|
@@ -2061,7 +2239,9 @@ def _build_agent_router(
|
|
|
2061
2239
|
return events[: max(1, limit)]
|
|
2062
2240
|
|
|
2063
2241
|
@router.get("/templates")
|
|
2064
|
-
async def list_agent_templates(
|
|
2242
|
+
async def list_agent_templates(
|
|
2243
|
+
include_non_public: bool = False,
|
|
2244
|
+
) -> list[_AgentTemplateSummary]:
|
|
2065
2245
|
"""
|
|
2066
2246
|
Return the executable agent templates registered in this pod.
|
|
2067
2247
|
|
|
@@ -2072,6 +2252,9 @@ def _build_agent_router(
|
|
|
2072
2252
|
|
|
2073
2253
|
How to use it:
|
|
2074
2254
|
- call from control-plane to aggregate template metadata across pods
|
|
2255
|
+
- pass `include_non_public=true` to also list internal agents
|
|
2256
|
+
(`AgentDefinition.public=False`) for tooling such as the self-test
|
|
2257
|
+
harness; the default catalog hides them (see AGENT-VISIBILITY-RFC)
|
|
2075
2258
|
|
|
2076
2259
|
Example:
|
|
2077
2260
|
- `GET /fred/agents/v2/agents/templates`
|
|
@@ -2088,6 +2271,7 @@ def _build_agent_router(
|
|
|
2088
2271
|
available_mcp_servers=_available_mcp_servers_for_definition(definition),
|
|
2089
2272
|
)
|
|
2090
2273
|
for definition in registry.values()
|
|
2274
|
+
if include_non_public or getattr(definition, "public", True)
|
|
2091
2275
|
]
|
|
2092
2276
|
|
|
2093
2277
|
@router.get("/mcp-catalog")
|
|
@@ -2559,15 +2743,15 @@ def _build_agent_router(
|
|
|
2559
2743
|
|
|
2560
2744
|
POST <configured base_url>/agents/execute
|
|
2561
2745
|
Authorization: Bearer <user JWT>
|
|
2562
|
-
Body: RuntimeExecuteRequest (agent_instance_id +
|
|
2746
|
+
Body: RuntimeExecuteRequest (agent_instance_id + runtime_context.team_id for managed exec)
|
|
2563
2747
|
Response: application/json containing the terminal runtime payload
|
|
2564
2748
|
|
|
2565
|
-
Security:
|
|
2566
|
-
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
-
|
|
2570
|
-
|
|
2749
|
+
Security (RUNTIME-07 rev. 2 — the pod is the execution authority):
|
|
2750
|
+
- Identity is the caller's Keycloak JWT (validated against Keycloak JWKS).
|
|
2751
|
+
- Authorization is a pod-side OpenFGA check on runtime_context.team_id,
|
|
2752
|
+
enforced per request. There is NO control-plane-signed grant.
|
|
2753
|
+
- Managed instances resolve their template+tuning from the control-plane
|
|
2754
|
+
team-scoped binding (config only).
|
|
2571
2755
|
|
|
2572
2756
|
Architectural note:
|
|
2573
2757
|
- This endpoint does not implement pod discovery or routing.
|
|
@@ -2576,42 +2760,14 @@ def _build_agent_router(
|
|
|
2576
2760
|
auth = http_request.headers.get("Authorization", "")
|
|
2577
2761
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
2578
2762
|
|
|
2579
|
-
expected_action = _expected_execution_action(request)
|
|
2580
|
-
|
|
2581
|
-
# Validate ExecutionGrant for managed execution paths
|
|
2582
|
-
try:
|
|
2583
|
-
validate_execution_grant(request, expected_action=expected_action)
|
|
2584
|
-
except ExecutionGrantViolation as exc:
|
|
2585
|
-
_emit_audit_event(
|
|
2586
|
-
container,
|
|
2587
|
-
"warning",
|
|
2588
|
-
"grant_validation_failed",
|
|
2589
|
-
agent_instance_id=request.agent_instance_id,
|
|
2590
|
-
user_id=request.effective_user_id(),
|
|
2591
|
-
action=expected_action.value,
|
|
2592
|
-
reason=str(exc),
|
|
2593
|
-
)
|
|
2594
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))
|
|
2595
|
-
if request.execution_grant is not None:
|
|
2596
|
-
_emit_audit_event(
|
|
2597
|
-
container,
|
|
2598
|
-
"info",
|
|
2599
|
-
"grant_validated",
|
|
2600
|
-
agent_instance_id=request.agent_instance_id,
|
|
2601
|
-
user_id=request.effective_user_id(),
|
|
2602
|
-
action=expected_action.value,
|
|
2603
|
-
)
|
|
2604
|
-
_validate_grant_user_correlation(request, authenticated_user, container)
|
|
2605
|
-
await _validate_session_checkpoint_access(request)
|
|
2606
|
-
|
|
2607
2763
|
exchange_id = str(uuid4())
|
|
2608
2764
|
turn_start = time.monotonic()
|
|
2609
|
-
internal_req =
|
|
2610
|
-
|
|
2611
|
-
|
|
2765
|
+
internal_req, target = await _authorize_and_resolve(
|
|
2766
|
+
request,
|
|
2767
|
+
authenticated_user=authenticated_user,
|
|
2768
|
+
container=container,
|
|
2612
2769
|
registry=registry,
|
|
2613
2770
|
access_token=access_token,
|
|
2614
|
-
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
2615
2771
|
)
|
|
2616
2772
|
payloads = [
|
|
2617
2773
|
payload
|
|
@@ -2680,41 +2836,14 @@ def _build_agent_router(
|
|
|
2680
2836
|
auth = http_request.headers.get("Authorization", "")
|
|
2681
2837
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
2682
2838
|
|
|
2683
|
-
expected_action = _expected_execution_action(request)
|
|
2684
|
-
|
|
2685
|
-
try:
|
|
2686
|
-
validate_execution_grant(request, expected_action=expected_action)
|
|
2687
|
-
except ExecutionGrantViolation as exc:
|
|
2688
|
-
_emit_audit_event(
|
|
2689
|
-
container,
|
|
2690
|
-
"warning",
|
|
2691
|
-
"grant_validation_failed",
|
|
2692
|
-
agent_instance_id=request.agent_instance_id,
|
|
2693
|
-
user_id=request.effective_user_id(),
|
|
2694
|
-
action=expected_action.value,
|
|
2695
|
-
reason=str(exc),
|
|
2696
|
-
)
|
|
2697
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))
|
|
2698
|
-
if request.execution_grant is not None:
|
|
2699
|
-
_emit_audit_event(
|
|
2700
|
-
container,
|
|
2701
|
-
"info",
|
|
2702
|
-
"grant_validated",
|
|
2703
|
-
agent_instance_id=request.agent_instance_id,
|
|
2704
|
-
user_id=request.effective_user_id(),
|
|
2705
|
-
action=expected_action.value,
|
|
2706
|
-
)
|
|
2707
|
-
_validate_grant_user_correlation(request, authenticated_user, container)
|
|
2708
|
-
await _validate_session_checkpoint_access(request)
|
|
2709
|
-
|
|
2710
2839
|
exchange_id = str(uuid4())
|
|
2711
2840
|
turn_start = time.monotonic()
|
|
2712
|
-
internal_req =
|
|
2713
|
-
|
|
2714
|
-
|
|
2841
|
+
internal_req, target = await _authorize_and_resolve(
|
|
2842
|
+
request,
|
|
2843
|
+
authenticated_user=authenticated_user,
|
|
2844
|
+
container=container,
|
|
2715
2845
|
registry=registry,
|
|
2716
2846
|
access_token=access_token,
|
|
2717
|
-
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
2718
2847
|
)
|
|
2719
2848
|
payloads = [
|
|
2720
2849
|
payload
|
|
@@ -2779,7 +2908,7 @@ def _build_agent_router(
|
|
|
2779
2908
|
|
|
2780
2909
|
POST <configured base_url>/agents/execute/stream
|
|
2781
2910
|
Authorization: Bearer <user JWT>
|
|
2782
|
-
Body: RuntimeExecuteRequest (agent_instance_id +
|
|
2911
|
+
Body: RuntimeExecuteRequest (agent_instance_id + runtime_context.team_id for managed exec)
|
|
2783
2912
|
Response: text/event-stream, each `data:` line is a RuntimeEvent JSON
|
|
2784
2913
|
|
|
2785
2914
|
Stream termination:
|
|
@@ -2807,40 +2936,12 @@ def _build_agent_router(
|
|
|
2807
2936
|
auth = http_request.headers.get("Authorization", "")
|
|
2808
2937
|
access_token = auth.removeprefix("Bearer ").strip() or None
|
|
2809
2938
|
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
validate_execution_grant(request, expected_action=expected_action)
|
|
2815
|
-
except ExecutionGrantViolation as exc:
|
|
2816
|
-
_emit_audit_event(
|
|
2817
|
-
container,
|
|
2818
|
-
"warning",
|
|
2819
|
-
"grant_validation_failed",
|
|
2820
|
-
agent_instance_id=request.agent_instance_id,
|
|
2821
|
-
user_id=request.effective_user_id(),
|
|
2822
|
-
action=expected_action.value,
|
|
2823
|
-
reason=str(exc),
|
|
2824
|
-
)
|
|
2825
|
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))
|
|
2826
|
-
if request.execution_grant is not None:
|
|
2827
|
-
_emit_audit_event(
|
|
2828
|
-
container,
|
|
2829
|
-
"info",
|
|
2830
|
-
"grant_validated",
|
|
2831
|
-
agent_instance_id=request.agent_instance_id,
|
|
2832
|
-
user_id=request.effective_user_id(),
|
|
2833
|
-
action=expected_action.value,
|
|
2834
|
-
)
|
|
2835
|
-
_validate_grant_user_correlation(request, authenticated_user, container)
|
|
2836
|
-
await _validate_session_checkpoint_access(request)
|
|
2837
|
-
|
|
2838
|
-
internal_req = _to_internal_request(request)
|
|
2839
|
-
target = await _resolve_agent_instance(
|
|
2840
|
-
request=internal_req,
|
|
2939
|
+
internal_req, target = await _authorize_and_resolve(
|
|
2940
|
+
request,
|
|
2941
|
+
authenticated_user=authenticated_user,
|
|
2942
|
+
container=container,
|
|
2841
2943
|
registry=registry,
|
|
2842
2944
|
access_token=access_token,
|
|
2843
|
-
control_plane_url=get_runtime_context().config.control_plane_url,
|
|
2844
2945
|
)
|
|
2845
2946
|
return StreamingResponse(
|
|
2846
2947
|
_stream(
|
|
@@ -2942,6 +3043,16 @@ def create_agent_app(
|
|
|
2942
3043
|
from fred_core.security.oidc import initialize_user_security
|
|
2943
3044
|
|
|
2944
3045
|
initialize_user_security(user_security)
|
|
3046
|
+
if security is not None:
|
|
3047
|
+
# Enforce the hardened profile (C3) at startup — fails closed.
|
|
3048
|
+
from fred_core.security.oidc import apply_security_profile
|
|
3049
|
+
|
|
3050
|
+
apply_security_profile(security)
|
|
3051
|
+
# Pod-side authorization engine (RUNTIME-07 rev. 2). The pod authorizes
|
|
3052
|
+
# every execution against OpenFGA; a disabled/Noop engine (dev) means
|
|
3053
|
+
# identity-only. Safe in all modes — the factory returns a Noop with a
|
|
3054
|
+
# KeycloackDisabled admin client when user/m2m auth is off.
|
|
3055
|
+
rebac_engine = rebac_factory(security) if security is not None else None
|
|
2945
3056
|
chat_factory = _build_chat_model_factory(config)
|
|
2946
3057
|
await container.initialize_sql()
|
|
2947
3058
|
container.start_metrics_exporter()
|
|
@@ -2963,6 +3074,10 @@ def create_agent_app(
|
|
|
2963
3074
|
history_store=history_store,
|
|
2964
3075
|
mcp_configuration=config.get_mcp_configuration(),
|
|
2965
3076
|
control_plane_url=config.platform.control_plane_url,
|
|
3077
|
+
rebac_engine=rebac_engine,
|
|
3078
|
+
security_profile=(
|
|
3079
|
+
security.profile if security is not None else None
|
|
3080
|
+
),
|
|
2966
3081
|
kpi_writer=container.get_kpi_writer(),
|
|
2967
3082
|
)
|
|
2968
3083
|
)
|
|
@@ -3019,10 +3134,19 @@ def create_agent_app(
|
|
|
3019
3134
|
app.include_router(api_router)
|
|
3020
3135
|
|
|
3021
3136
|
if config.app.openai_compat:
|
|
3137
|
+
# F-A: the OpenAI-compat surface executes by agent_id (direct template),
|
|
3138
|
+
# which is forbidden under c3. Fail closed rather than expose it there.
|
|
3139
|
+
if security is not None and security.profile == "c3":
|
|
3140
|
+
raise RuntimeError(
|
|
3141
|
+
"security.profile='c3' forbids the OpenAI-compat surface: "
|
|
3142
|
+
"/v1/chat/completions executes by agent_id (direct template), "
|
|
3143
|
+
"which is not permitted under c3. Set app.openai_compat=false."
|
|
3144
|
+
)
|
|
3022
3145
|
from .openai_compat_router import create_openai_compat_router
|
|
3023
3146
|
|
|
3024
3147
|
openai_router = create_openai_compat_router(
|
|
3025
|
-
registry,
|
|
3148
|
+
registry,
|
|
3149
|
+
security_enabled=security_enabled,
|
|
3026
3150
|
)
|
|
3027
3151
|
app.include_router(openai_router, prefix="/v1")
|
|
3028
3152
|
logger.info("[fred-runtime] OpenAI-compat endpoints enabled at /v1")
|
|
@@ -121,7 +121,7 @@ class PodAppConfig(BaseModel):
|
|
|
121
121
|
),
|
|
122
122
|
)
|
|
123
123
|
gcu_version: str | None = None
|
|
124
|
-
openai_compat: bool =
|
|
124
|
+
openai_compat: bool = False
|
|
125
125
|
"""
|
|
126
126
|
Enable the OpenAI-compatible /v1/chat/completions and /v1/models endpoints.
|
|
127
127
|
|
|
@@ -134,9 +134,12 @@ class PodAppConfig(BaseModel):
|
|
|
134
134
|
citations, HITL) is carried in a top-level `fred` key on each SSE chunk
|
|
135
135
|
and silently ignored by standard OpenAI clients.
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
OFF by default (RUNTIME-07 rev. 2, finding F-A): this surface executes by
|
|
138
|
+
`agent_id` (direct template), which is forbidden under the c3 profile, and it
|
|
139
|
+
must be authorized per request. When enabled it is gated by the same Keycloak
|
|
140
|
+
JWT + pod-side OpenFGA check (requires the `X-Fred-Team-Id` header). The c3
|
|
141
|
+
profile FAILS CLOSED at startup if this surface is enabled (see
|
|
142
|
+
`apply_security_profile`). Enable only for dev / eval harnesses.
|
|
140
143
|
"""
|
|
141
144
|
|
|
142
145
|
|
|
@@ -288,7 +291,8 @@ class PodPlatformConfig(BaseModel):
|
|
|
288
291
|
|
|
289
292
|
How to use it:
|
|
290
293
|
- set `control_plane_url` when the pod should accept `agent_instance_id`
|
|
291
|
-
execution requests
|
|
294
|
+
execution requests (the pod resolves the instance's template+tuning from
|
|
295
|
+
the control-plane team-scoped binding)
|
|
292
296
|
|
|
293
297
|
Example:
|
|
294
298
|
- `PodPlatformConfig(control_plane_url="http://localhost:8222/control-plane/v1")`
|