fred-runtime 2.0.7__tar.gz → 2.0.8__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.
Files changed (106) hide show
  1. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/PKG-INFO +1 -1
  2. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/agent_app.py +127 -49
  3. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/PKG-INFO +1 -1
  4. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/pyproject.toml +1 -1
  5. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_history.py +131 -2
  6. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/README.md +0 -0
  7. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/__init__.py +0 -0
  8. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/__init__.py +0 -0
  9. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/_catalogs.py +0 -0
  10. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/config.py +0 -0
  11. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/config_loader.py +0 -0
  12. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/container.py +0 -0
  13. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/context.py +0 -0
  14. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/dependencies.py +0 -0
  15. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/mcp_config.py +0 -0
  16. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/observability_factory.py +0 -0
  17. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/app/openai_compat_router.py +0 -0
  18. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/__init__.py +0 -0
  19. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/completion.py +0 -0
  20. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/entrypoint.py +0 -0
  21. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/history_display.py +0 -0
  22. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/kpi_display.py +0 -0
  23. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/pod_client.py +0 -0
  24. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/repl.py +0 -0
  25. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/repl_helpers.py +0 -0
  26. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/cli/url_helpers.py +0 -0
  27. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/client.py +0 -0
  28. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/__init__.py +0 -0
  29. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/context_aware_tool.py +0 -0
  30. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_base_client.py +0 -0
  31. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_fast_text_client.py +0 -0
  32. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_http_client.py +0 -0
  33. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_logs_client.py +0 -0
  34. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  35. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  36. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/kf_workspace_client.py +0 -0
  37. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/mcp_interceptors.py +0 -0
  38. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/mcp_runtime.py +0 -0
  39. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/mcp_toolkit.py +0 -0
  40. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/mcp_utils.py +0 -0
  41. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/structures.py +0 -0
  42. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/token_expiry.py +0 -0
  43. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/common/tool_node_utils.py +0 -0
  44. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/deep/__init__.py +0 -0
  45. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/deep/deep_runtime.py +0 -0
  46. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/eval/__init__.py +0 -0
  47. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/eval/collector.py +0 -0
  48. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/graph/__init__.py +0 -0
  49. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/graph/graph_runtime.py +0 -0
  50. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/integrations/__init__.py +0 -0
  51. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  52. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/integrations/v2_runtime/adapters.py +0 -0
  53. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/model_routing/__init__.py +0 -0
  54. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/model_routing/catalog.py +0 -0
  55. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/model_routing/contracts.py +0 -0
  56. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/model_routing/provider.py +0 -0
  57. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/model_routing/resolver.py +0 -0
  58. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/__init__.py +0 -0
  59. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_langchain_adapter.py +0 -0
  60. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_message_codec.py +0 -0
  61. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_model_adapter.py +0 -0
  62. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_prompting.py +0 -0
  63. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_runtime.py +0 -0
  64. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_stream_adapter.py +0 -0
  65. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tool_binding.py +0 -0
  66. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tool_loop.py +0 -0
  67. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tool_rendering.py +0 -0
  68. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tool_resolution.py +0 -0
  69. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tool_utils.py +0 -0
  70. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/react/react_tracing.py +0 -0
  71. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_context.py +0 -0
  72. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/__init__.py +0 -0
  73. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/checkpoints.py +0 -0
  74. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/model_metadata.py +0 -0
  75. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  76. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  77. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  78. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/support/__init__.py +0 -0
  79. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/support/filesystem_context.py +0 -0
  80. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/support/tool_approval.py +0 -0
  81. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime/support/tool_loop.py +0 -0
  82. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/SOURCES.txt +0 -0
  83. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/dependency_links.txt +0 -0
  84. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/entry_points.txt +0 -0
  85. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/requires.txt +0 -0
  86. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/fred_runtime.egg-info/top_level.txt +0 -0
  87. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/setup.cfg +0 -0
  88. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_agent_app.py +0 -0
  89. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_client.py +0 -0
  90. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_config_loader.py +0 -0
  91. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_context.py +0 -0
  92. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_conversational_memory.py +0 -0
  93. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_eval_collector.py +0 -0
  94. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_eval_trace.py +0 -0
  95. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_graph_runtime_observability.py +0 -0
  96. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_kf_workspace_client.py +0 -0
  97. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_kpi_display.py +0 -0
  98. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_mcp_config.py +0 -0
  99. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_model_routing.py +0 -0
  100. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_openai_compat_router.py +0 -0
  101. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_pod_client.py +0 -0
  102. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_repl_helpers.py +0 -0
  103. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_smoke.py +0 -0
  104. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_token_expiry.py +0 -0
  105. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/tests/test_url_helpers.py +0 -0
  106. {fred_runtime-2.0.7 → fred_runtime-2.0.8}/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: 2.0.7
3
+ Version: 2.0.8
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
@@ -814,6 +814,7 @@ class _AgentTemplateSummary(BaseModel):
814
814
  template_agent_id: str
815
815
  title: str
816
816
  description: str
817
+ description_by_lang: dict[str, str] | None = None
817
818
  kind: ExecutionCategory
818
819
  default_tuning: AgentTuning
819
820
  available_mcp_servers: list[MCPServerConfiguration] = Field(default_factory=list)
@@ -2060,6 +2061,7 @@ def _build_agent_router(
2060
2061
  template_agent_id=definition.agent_id,
2061
2062
  title=definition.role,
2062
2063
  description=definition.description,
2064
+ description_by_lang=getattr(definition, "description_by_lang", None),
2063
2065
  kind=definition.execution_category,
2064
2066
  default_tuning=_definition_to_agent_tuning(definition),
2065
2067
  available_mcp_servers=_available_mcp_servers_for_definition(definition),
@@ -2101,22 +2103,20 @@ def _build_agent_router(
2101
2103
  ]
2102
2104
  )
2103
2105
 
2104
- @router.get("/sessions", dependencies=_auth_deps)
2105
- async def list_sessions(user_id: str) -> list[str]:
2106
+ @router.get("/sessions")
2107
+ async def list_sessions(
2108
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2109
+ user_id: str | None = None,
2110
+ ) -> list[str]:
2106
2111
  """
2107
- Return the session IDs for a user, most recent first.
2112
+ Return the session IDs for the authenticated user, most recent first.
2108
2113
 
2109
- GET <configured base_url>/agents/sessions?user_id=<user_id>
2110
- Authorization: Bearer <user JWT> (same auth as execute endpoints)
2111
- Response: JSON array of session_id strings
2112
-
2113
- Why this endpoint exists:
2114
- - the UI needs to list past conversations for a returning user
2115
- - the checkpointer has no user_id index; only the history store does
2114
+ GET <configured base_url>/agents/sessions
2115
+ Authorization: Bearer <user JWT>
2116
2116
 
2117
- How to use it:
2118
- - GET /agents/sessions?user_id=alice
2119
- - returns ["session-3", "session-1", ...] most recent first
2117
+ Security: user identity is always extracted from the JWT token.
2118
+ The user_id query parameter is accepted only in dev mode (security
2119
+ disabled) for CLI convenience; it is ignored when security is enabled.
2120
2120
  """
2121
2121
  history_store = get_runtime_context().config.history_store
2122
2122
  if history_store is None:
@@ -2124,29 +2124,30 @@ def _build_agent_router(
2124
2124
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
2125
2125
  detail="No history store configured — session listing is unavailable.",
2126
2126
  )
2127
- return await history_store.list_sessions(user_id=user_id)
2127
+ effective_uid = caller.uid if caller is not None else user_id
2128
+ if effective_uid is None:
2129
+ return []
2130
+ return await history_store.list_sessions(user_id=effective_uid)
2128
2131
 
2129
2132
  @router.get(
2130
2133
  "/sessions/{session_id}/messages",
2131
- dependencies=_auth_deps,
2132
2134
  response_model=list[ChatMessage],
2133
2135
  )
2134
- async def get_session_messages(session_id: str) -> list[ChatMessage]:
2136
+ async def get_session_messages(
2137
+ session_id: str,
2138
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2139
+ ) -> list[ChatMessage]:
2135
2140
  """
2136
2141
  Return the conversation history for a session as a flat message list.
2137
2142
 
2138
2143
  GET <configured base_url>/agents/sessions/{session_id}/messages
2139
- Authorization: Bearer <user JWT> (same auth as execute endpoints)
2140
- Response: JSON array of ChatMessage objects (role/channel/parts/metadata).
2144
+ Authorization: Bearer <user JWT>
2141
2145
 
2142
- Why the history store is the source of truth:
2143
- - the history store writes one row per message, keyed by (session_id, rank)
2144
- - it is agent-type-agnostic and works for both ReAct and Graph agents
2145
- - the checkpointer (former source) only works for agents with a ``messages``
2146
- channel in state and is not queryable per user_id
2146
+ Security: only rows belonging to the authenticated user are returned.
2147
+ Returns [] when the session does not exist or belongs to another user
2148
+ callers cannot distinguish the two cases by design.
2147
2149
 
2148
2150
  Returns 503 when no history store is configured (stateless pod mode).
2149
- Returns [] when the session exists but has no rows yet.
2150
2151
  """
2151
2152
  history_store = get_runtime_context().config.history_store
2152
2153
  if history_store is None:
@@ -2154,27 +2155,29 @@ def _build_agent_router(
2154
2155
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
2155
2156
  detail="No history store configured — session history is unavailable.",
2156
2157
  )
2157
- return await history_store.get(session_id=session_id)
2158
+ caller_uid = caller.uid if caller is not None else None
2159
+ return await history_store.get(session_id=session_id, user_id=caller_uid)
2158
2160
 
2159
2161
  @router.delete(
2160
2162
  "/sessions/{session_id}",
2161
- dependencies=_auth_deps,
2162
2163
  status_code=status.HTTP_200_OK,
2163
2164
  )
2164
- async def delete_session_history(session_id: str) -> dict[str, int]:
2165
+ async def delete_session_history(
2166
+ session_id: str,
2167
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2168
+ ) -> dict[str, int]:
2165
2169
  """
2166
- Permanently delete all history rows for a session.
2170
+ Permanently delete history rows for a session.
2167
2171
 
2168
2172
  DELETE <base_url>/agents/sessions/{session_id}
2169
2173
 
2170
- Why this endpoint exists:
2171
- - developers need to clean up test sessions from the CLI without
2172
- restarting the pod or connecting to the database directly
2173
- - devops need to reclaim storage from stale or abandoned conversations
2174
- - checkpoint state is NOT touched; delete the checkpoint separately
2175
- via DELETE /agents/checkpoints/{session_id} if required
2174
+ Security: only rows belonging to the authenticated user are deleted.
2175
+ Returns {"deleted": 0} when the session does not exist or belongs to
2176
+ another user callers cannot distinguish the two cases by design.
2177
+
2178
+ Checkpoint state is NOT touched; delete separately via
2179
+ DELETE /agents/checkpoints/{session_id} if required.
2176
2180
 
2177
- Returns {"deleted": N} where N is the number of rows removed.
2178
2181
  Returns 503 when no history store is configured.
2179
2182
  """
2180
2183
  history_store = get_runtime_context().config.history_store
@@ -2183,7 +2186,10 @@ def _build_agent_router(
2183
2186
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
2184
2187
  detail="No history store configured — session history is unavailable.",
2185
2188
  )
2186
- count = await history_store.delete_session(session_id=session_id)
2189
+ caller_uid = caller.uid if caller is not None else None
2190
+ count = await history_store.delete_session(
2191
+ session_id=session_id, user_id=caller_uid
2192
+ )
2187
2193
  return {"deleted": count}
2188
2194
 
2189
2195
  # ------------------------------------------------------------------
@@ -2199,8 +2205,36 @@ def _build_agent_router(
2199
2205
  )
2200
2206
  return cp
2201
2207
 
2202
- @router.get("/checkpoints", dependencies=_auth_deps)
2208
+ def _get_history_store_for_owned_access(
2209
+ caller: KeycloakUser | None,
2210
+ ) -> HistoryStorePort | None:
2211
+ """Return the history store used as ownership oracle, failing closed.
2212
+
2213
+ Why this helper exists:
2214
+ - checkpoint tables do not carry user_id, so ownership checks depend on
2215
+ the history store
2216
+ - when security is enabled, proceeding without that oracle would leak
2217
+ checkpoint visibility across users
2218
+
2219
+ How to use it:
2220
+ - call from checkpoint endpoints before listing or mutating per-session
2221
+ checkpoint data
2222
+ """
2223
+ if caller is None:
2224
+ return None
2225
+ history_store = get_runtime_context().config.history_store
2226
+ if history_store is None:
2227
+ raise HTTPException(
2228
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
2229
+ detail=(
2230
+ "No history store configured — ownership checks are unavailable."
2231
+ ),
2232
+ )
2233
+ return history_store
2234
+
2235
+ @router.get("/checkpoints")
2203
2236
  async def list_checkpoint_threads(
2237
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2204
2238
  limit: int = 100,
2205
2239
  ) -> list[_CheckpointThreadSummary]:
2206
2240
  """
@@ -2271,6 +2305,15 @@ def _build_agent_router(
2271
2305
  .limit(limit)
2272
2306
  )
2273
2307
  rows = (await conn.execute(stmt)).fetchall()
2308
+
2309
+ # Scope to the caller's own sessions using the history store as the
2310
+ # ownership oracle (checkpoint tables carry no user_id column).
2311
+ caller_uid = caller.uid if caller is not None else None
2312
+ history_store = _get_history_store_for_owned_access(caller)
2313
+ if caller_uid is not None and history_store is not None:
2314
+ owned = set(await history_store.list_sessions(user_id=caller_uid))
2315
+ rows = [r for r in rows if str(r.thread_id) in owned]
2316
+
2274
2317
  return [
2275
2318
  _CheckpointThreadSummary(
2276
2319
  session_id=str(row.thread_id),
@@ -2338,8 +2381,11 @@ def _build_agent_router(
2338
2381
  blob_bytes_approx=int(blob_bytes),
2339
2382
  )
2340
2383
 
2341
- @router.get("/checkpoints/{session_id}", dependencies=_auth_deps)
2342
- async def get_checkpoint_thread(session_id: str) -> _CheckpointThreadDetail:
2384
+ @router.get("/checkpoints/{session_id}")
2385
+ async def get_checkpoint_thread(
2386
+ session_id: str,
2387
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2388
+ ) -> _CheckpointThreadDetail:
2343
2389
  """
2344
2390
  Return all checkpoints for one session, newest first.
2345
2391
 
@@ -2360,7 +2406,20 @@ def _build_agent_router(
2360
2406
 
2361
2407
  Returns 503 when no checkpointer is configured.
2362
2408
  Returns an empty checkpoints list when the session has no rows.
2409
+ Returns 403 when the session does not belong to the authenticated user.
2363
2410
  """
2411
+ caller_uid = caller.uid if caller is not None else None
2412
+ history_store = _get_history_store_for_owned_access(caller)
2413
+ if (
2414
+ caller_uid is not None
2415
+ and history_store is not None
2416
+ and not await history_store.session_belongs_to_user(session_id, caller_uid)
2417
+ ):
2418
+ raise HTTPException(
2419
+ status_code=status.HTTP_403_FORBIDDEN,
2420
+ detail="Access denied.",
2421
+ )
2422
+
2364
2423
  from sqlalchemy import desc, func, select
2365
2424
 
2366
2425
  cp = _get_checkpointer()
@@ -2429,25 +2488,38 @@ def _build_agent_router(
2429
2488
 
2430
2489
  @router.delete(
2431
2490
  "/checkpoints/{session_id}",
2432
- dependencies=_auth_deps,
2433
2491
  status_code=status.HTTP_204_NO_CONTENT,
2434
2492
  response_model=None,
2435
2493
  )
2436
- async def delete_checkpoint_thread(session_id: str) -> None:
2494
+ async def delete_checkpoint_thread(
2495
+ session_id: str,
2496
+ caller: KeycloakUser | None = Depends(_authenticated_user),
2497
+ ) -> None:
2437
2498
  """
2438
2499
  Purge all checkpoint data for one session.
2439
2500
 
2440
2501
  DELETE <base_url>/agents/checkpoints/{session_id}
2441
2502
 
2442
- Deletes all rows in the checkpoints, blobs, and writes tables for the
2443
- given session_id. This is irreversible the agent will lose the ability
2444
- to resume from any prior HITL pause or conversation state for this session.
2503
+ Security: the session must belong to the authenticated user.
2504
+ Returns 403 when ownership cannot be confirmed via the history store.
2445
2505
 
2446
- History store rows (session_history) are NOT deleted only checkpoint
2447
- state is purged. Use this to reclaim storage from stale or test sessions.
2506
+ Deletes all rows in the checkpoints, blobs, and writes tables.
2507
+ History store rows are NOT deleted use DELETE /sessions/{session_id}
2508
+ to remove those separately.
2448
2509
 
2449
- Returns 204 on success, 503 when no checkpointer is configured.
2510
+ Returns 204 on success, 403 when not owned, 503 when no checkpointer.
2450
2511
  """
2512
+ caller_uid = caller.uid if caller is not None else None
2513
+ history_store = _get_history_store_for_owned_access(caller)
2514
+ if (
2515
+ caller_uid is not None
2516
+ and history_store is not None
2517
+ and not await history_store.session_belongs_to_user(session_id, caller_uid)
2518
+ ):
2519
+ raise HTTPException(
2520
+ status_code=status.HTTP_403_FORBIDDEN,
2521
+ detail="Access denied.",
2522
+ )
2451
2523
  cp = _get_checkpointer()
2452
2524
  await cp.adelete_thread(session_id)
2453
2525
 
@@ -2850,6 +2922,12 @@ def create_agent_app(
2850
2922
  await container.initialize_sql()
2851
2923
  container.start_metrics_exporter()
2852
2924
  await container.start_kpi_tasks()
2925
+ checkpointer = container.get_checkpointer()
2926
+ history_store = container.get_history_store()
2927
+ if (checkpointer is None) != (history_store is None):
2928
+ raise RuntimeError(
2929
+ "Invalid runtime storage state: checkpointer and history store must be configured together."
2930
+ )
2853
2931
  set_runtime_context(
2854
2932
  FredRuntimeContext(
2855
2933
  RuntimeConfig(
@@ -2857,8 +2935,8 @@ def create_agent_app(
2857
2935
  service_name=config.app.name,
2858
2936
  timeouts=config.ai.timeout,
2859
2937
  chat_model_factory=chat_factory,
2860
- checkpointer=container.get_checkpointer(),
2861
- history_store=container.get_history_store(),
2938
+ checkpointer=checkpointer,
2939
+ history_store=history_store,
2862
2940
  mcp_configuration=config.get_mcp_configuration(),
2863
2941
  control_plane_url=config.platform.control_plane_url,
2864
2942
  kpi_writer=container.get_kpi_writer(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 2.0.7
3
+ Version: 2.0.8
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fred-runtime"
3
- version = "2.0.7"
3
+ version = "2.0.8"
4
4
  description = "Runtime adapters and infrastructure wiring for Fred v2 agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.13"
@@ -331,7 +331,13 @@ def test_postgres_history_store_satisfies_history_store_port() -> None:
331
331
  """
332
332
  from fred_core.history.postgres_history_store import PostgresHistoryStore
333
333
 
334
- required = {"save", "get", "list_sessions", "delete_session"}
334
+ required = {
335
+ "save",
336
+ "get",
337
+ "list_sessions",
338
+ "delete_session",
339
+ "session_belongs_to_user",
340
+ }
335
341
 
336
342
  # Implementation must have all required methods.
337
343
  impl_attrs = set(dir(PostgresHistoryStore))
@@ -423,4 +429,127 @@ def test_get_session_messages_returns_empty_list_for_unknown_session(
423
429
 
424
430
  assert response.status_code == 200
425
431
  assert response.json() == []
426
- mock_store.get.assert_awaited_once_with(session_id="session-new")
432
+ # In dev mode (security disabled) caller_uid is None — user_id=None is passed
433
+ # so the store returns all rows for the session (no ownership filter applied).
434
+ mock_store.get.assert_awaited_once_with(session_id="session-new", user_id=None)
435
+
436
+
437
+ # ---------------------------------------------------------------------------
438
+ # Test 5 — Session endpoint user isolation (security behaviour)
439
+ # ---------------------------------------------------------------------------
440
+
441
+
442
+ def _make_app_with_user(monkeypatch, tmp_path, user_uid: str):
443
+ """
444
+ Build a test app where _authenticated_user is overridden to return a fixed
445
+ KeycloakUser. Used to verify ownership enforcement without real Keycloak.
446
+ """
447
+ from fred_core.security.structure import KeycloakUser
448
+
449
+ fake_user = KeycloakUser(uid=user_uid, username=user_uid, roles=[])
450
+
451
+ # Patch _make_user_dependency before create_routes() runs so the closure
452
+ # produced by create_agent_app picks up our fake-user factory.
453
+ monkeypatch.setattr(
454
+ agent_app_module,
455
+ "_make_user_dependency",
456
+ lambda _fn, _enabled: lambda: fake_user,
457
+ )
458
+ return _make_app(monkeypatch, tmp_path), fake_user
459
+
460
+
461
+ def test_list_sessions_uses_jwt_uid_not_query_param(monkeypatch, tmp_path) -> None:
462
+ """
463
+ GET /agents/sessions must use the JWT-extracted uid, ignoring any user_id
464
+ query parameter supplied by the caller.
465
+
466
+ Why this test exists:
467
+ - the old implementation accepted user_id as a caller-supplied query param,
468
+ allowing any authenticated user to list another user's sessions
469
+ - after the fix, the JWT uid is always used when security is enabled
470
+ - this test verifies that a user_id query param is silently ignored when the
471
+ caller is authenticated
472
+
473
+ How to use it:
474
+ - offline: _make_user_dependency is monkeypatched to inject a fixed user
475
+ """
476
+ app, fake_user = _make_app_with_user(monkeypatch, tmp_path, "alice-uid")
477
+
478
+ mock_store = AsyncMock()
479
+ mock_store.list_sessions = AsyncMock(return_value=["s-alice-1", "s-alice-2"])
480
+ mock_ctx = RuntimeContext(
481
+ RuntimeConfig(
482
+ knowledge_flow_url="http://localhost:8111/knowledge-flow/v1",
483
+ history_store=mock_store,
484
+ )
485
+ )
486
+
487
+ with TestClient(app) as client:
488
+ monkeypatch.setattr(agent_app_module, "get_runtime_context", lambda: mock_ctx)
489
+ # Pass a different user_id in the query string — must be ignored.
490
+ response = client.get("/pod/v1/agents/sessions?user_id=liam-uid")
491
+
492
+ assert response.status_code == 200
493
+ # Store must have been called with alice's uid, not liam's.
494
+ mock_store.list_sessions.assert_awaited_once_with(user_id="alice-uid")
495
+
496
+
497
+ def test_get_session_messages_passes_caller_uid_to_store(monkeypatch, tmp_path) -> None:
498
+ """
499
+ GET /sessions/{id}/messages must pass the JWT uid as user_id to the history
500
+ store so only the caller's rows are returned.
501
+
502
+ Why this test exists:
503
+ - verifies that cross-user message history access is blocked at the store call
504
+ level — a user cannot read another user's conversation by knowing a session_id
505
+ """
506
+ app, fake_user = _make_app_with_user(monkeypatch, tmp_path, "alice-uid")
507
+
508
+ mock_store = AsyncMock()
509
+ mock_store.get = AsyncMock(return_value=[])
510
+ mock_ctx = RuntimeContext(
511
+ RuntimeConfig(
512
+ knowledge_flow_url="http://localhost:8111/knowledge-flow/v1",
513
+ history_store=mock_store,
514
+ )
515
+ )
516
+
517
+ with TestClient(app) as client:
518
+ monkeypatch.setattr(agent_app_module, "get_runtime_context", lambda: mock_ctx)
519
+ response = client.get("/pod/v1/agents/sessions/session-liam/messages")
520
+
521
+ assert response.status_code == 200
522
+ mock_store.get.assert_awaited_once_with(
523
+ session_id="session-liam", user_id="alice-uid"
524
+ )
525
+
526
+
527
+ def test_delete_session_passes_caller_uid_to_store(monkeypatch, tmp_path) -> None:
528
+ """
529
+ DELETE /sessions/{id} must pass the JWT uid as user_id so only the caller's
530
+ rows are deleted — prevents one user from deleting another user's history.
531
+
532
+ Why this test exists:
533
+ - delete without user scoping is an irreversible cross-user data destruction
534
+ vector; this verifies the ownership filter is applied at the store layer
535
+ """
536
+ app, fake_user = _make_app_with_user(monkeypatch, tmp_path, "alice-uid")
537
+
538
+ mock_store = AsyncMock()
539
+ mock_store.delete_session = AsyncMock(return_value=3)
540
+ mock_ctx = RuntimeContext(
541
+ RuntimeConfig(
542
+ knowledge_flow_url="http://localhost:8111/knowledge-flow/v1",
543
+ history_store=mock_store,
544
+ )
545
+ )
546
+
547
+ with TestClient(app) as client:
548
+ monkeypatch.setattr(agent_app_module, "get_runtime_context", lambda: mock_ctx)
549
+ response = client.delete("/pod/v1/agents/sessions/session-liam")
550
+
551
+ assert response.status_code == 200
552
+ assert response.json() == {"deleted": 3}
553
+ mock_store.delete_session.assert_awaited_once_with(
554
+ session_id="session-liam", user_id="alice-uid"
555
+ )
File without changes
File without changes