fred-runtime 3.3.6__tar.gz → 3.4.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.
Files changed (164) hide show
  1. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/PKG-INFO +1 -1
  2. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/__main__.py +0 -1
  3. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/agent_app.py +307 -78
  4. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/context.py +34 -6
  5. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/openai_compat_router.py +4 -0
  6. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/__init__.py +2 -0
  7. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/assembly.py +79 -13
  8. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo.py +28 -31
  9. fred_runtime-3.4.0/fred_runtime/capabilities/document_access/capability.py +886 -0
  10. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/errors.py +16 -0
  11. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/mcp.py +12 -2
  12. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/registry.py +61 -0
  13. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/context_aware_tool.py +6 -2
  14. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_base_client.py +20 -1
  15. fred_runtime-3.4.0/fred_runtime/common/kf_document_client.py +213 -0
  16. fred_runtime-3.4.0/fred_runtime/common/kf_tag_client.py +186 -0
  17. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/graph/graph_runtime.py +214 -7
  18. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/v2_runtime/adapters.py +257 -0
  19. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/__init__.py +4 -0
  20. fred_runtime-3.4.0/fred_runtime/model_routing/authz.py +104 -0
  21. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/contracts.py +40 -0
  22. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/provider.py +68 -3
  23. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/resolver.py +54 -0
  24. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/tool_observability.py +68 -0
  25. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_prompting.py +49 -6
  26. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_runtime.py +11 -7
  27. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_loop.py +1 -1
  28. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_context.py +15 -1
  29. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/sql_checkpointer.py +22 -0
  30. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/tool_loop.py +39 -3
  31. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/PKG-INFO +1 -1
  32. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/SOURCES.txt +8 -0
  33. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/pyproject.toml +1 -1
  34. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_agent_app.py +880 -18
  35. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_agent_1973.py +212 -18
  36. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_chat_parts_1977.py +5 -2
  37. fred_runtime-3.4.0/tests/test_capability_document_access_1906.py +1050 -0
  38. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_endpoints_1974.py +122 -1
  39. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_registry_1973.py +108 -0
  40. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_selection_1974.py +4 -0
  41. fred_runtime-3.4.0/tests/test_capability_tool_return_convention.py +213 -0
  42. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_context.py +75 -0
  43. fred_runtime-3.4.0/tests/test_graph_capability_bridge.py +651 -0
  44. fred_runtime-3.4.0/tests/test_model_enforcement.py +385 -0
  45. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_model_routing.py +113 -1
  46. fred_runtime-3.4.0/tests/test_models_catalog_projection.py +139 -0
  47. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_loop_regressions_1972.py +22 -13
  48. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_prompting.py +23 -3
  49. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_sql_checkpointer_owner.py +34 -0
  50. fred_runtime-3.4.0/tests/test_tool_loop_trim.py +105 -0
  51. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_tool_observability_middleware.py +179 -0
  52. fred_runtime-3.3.6/fred_runtime/capabilities/document_access/capability.py +0 -395
  53. fred_runtime-3.3.6/tests/test_capability_document_access_1906.py +0 -515
  54. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/README.md +0 -0
  55. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/__init__.py +0 -0
  56. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/__init__.py +0 -0
  57. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/_catalogs.py +0 -0
  58. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/config.py +0 -0
  59. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/config_loader.py +0 -0
  60. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/container.py +0 -0
  61. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/dependencies.py +0 -0
  62. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/mcp_config.py +0 -0
  63. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/app/observability_factory.py +0 -0
  64. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/assets.py +0 -0
  65. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo_migrations/env.py +0 -0
  66. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/demo_migrations/versions/d1e2m0a0b0c0_create_demo_echo_notes.py +0 -0
  67. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/document_access/__init__.py +0 -0
  68. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/capabilities/openapi_dump.py +0 -0
  69. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/__init__.py +0 -0
  70. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/completion.py +0 -0
  71. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/entrypoint.py +0 -0
  72. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/history_display.py +0 -0
  73. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/kpi_display.py +0 -0
  74. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/pod_client.py +0 -0
  75. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/repl.py +0 -0
  76. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/repl_helpers.py +0 -0
  77. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/cli/url_helpers.py +0 -0
  78. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/client.py +0 -0
  79. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/__init__.py +0 -0
  80. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_fast_text_client.py +0 -0
  81. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_http_client.py +0 -0
  82. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  83. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  84. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/kf_workspace_client.py +0 -0
  85. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_interceptors.py +0 -0
  86. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_runtime.py +0 -0
  87. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_toolkit.py +0 -0
  88. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/mcp_utils.py +0 -0
  89. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/structures.py +0 -0
  90. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/token_expiry.py +0 -0
  91. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/common/tool_node_utils.py +0 -0
  92. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/deep/__init__.py +0 -0
  93. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/deep/deep_runtime.py +0 -0
  94. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/eval/__init__.py +0 -0
  95. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/eval/collector.py +0 -0
  96. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/graph/__init__.py +0 -0
  97. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/__init__.py +0 -0
  98. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/inprocess_toolkit_registry.py +0 -0
  99. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/kf_vector_search/__init__.py +0 -0
  100. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/kf_vector_search/toolkit.py +0 -0
  101. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  102. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/migrations.py +0 -0
  103. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/model_routing/catalog.py +0 -0
  104. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/__init__.py +0 -0
  105. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/__init__.py +0 -0
  106. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/checkpoint_hygiene.py +0 -0
  107. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/dynamic_prompt.py +0 -0
  108. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/frame.py +0 -0
  109. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/hitl.py +0 -0
  110. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/model_routing.py +0 -0
  111. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/shared.py +0 -0
  112. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/middleware/tracing_kpi.py +0 -0
  113. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_langchain_adapter.py +0 -0
  114. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_message_codec.py +0 -0
  115. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_model_adapter.py +0 -0
  116. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_stream_adapter.py +0 -0
  117. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_binding.py +0 -0
  118. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_rendering.py +0 -0
  119. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_resolution.py +0 -0
  120. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tool_utils.py +0 -0
  121. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/react/react_tracing.py +0 -0
  122. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/__init__.py +0 -0
  123. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/checkpoints.py +0 -0
  124. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/model_metadata.py +0 -0
  125. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  126. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  127. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/__init__.py +0 -0
  128. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/filesystem_context.py +0 -0
  129. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime/support/thinking.py +0 -0
  130. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/dependency_links.txt +0 -0
  131. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/entry_points.txt +0 -0
  132. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/requires.txt +0 -0
  133. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/fred_runtime.egg-info/top_level.txt +0 -0
  134. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/setup.cfg +0 -0
  135. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_chat_controls_1976.py +0 -0
  136. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_capability_routes_tables_1979.py +0 -0
  137. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_client.py +0 -0
  138. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_config_loader.py +0 -0
  139. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_context_aware_tool.py +0 -0
  140. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_conversational_memory.py +0 -0
  141. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_deep_agent_middleware.py +0 -0
  142. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_eval_collector.py +0 -0
  143. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_eval_trace.py +0 -0
  144. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_fred_workspace_fs.py +0 -0
  145. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_graph_runtime_invoke_agent.py +0 -0
  146. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_graph_runtime_observability.py +0 -0
  147. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_history.py +0 -0
  148. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kf_vector_search_toolkit.py +0 -0
  149. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kf_workspace_client.py +0 -0
  150. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_knowledge_search_tool_invoker_sources.py +0 -0
  151. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_kpi_display.py +0 -0
  152. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_mcp_config.py +0 -0
  153. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_openai_compat_router.py +0 -0
  154. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_pod_client.py +0 -0
  155. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_middleware_frame.py +0 -0
  156. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_react_thinking.py +0 -0
  157. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_repl_helpers.py +0 -0
  158. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_runtime_context_prompt_injection.py +0 -0
  159. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_smoke.py +0 -0
  160. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_spike_capability_state_1971.py +0 -0
  161. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_token_expiry.py +0 -0
  162. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_tracing_kpi_content_confinement.py +0 -0
  163. {fred_runtime-3.3.6 → fred_runtime-3.4.0}/tests/test_url_helpers.py +0 -0
  164. {fred_runtime-3.3.6 → fred_runtime-3.4.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.6
3
+ Version: 3.4.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
@@ -77,7 +77,6 @@ def main(argv: list[str] | None = None) -> int:
77
77
  if args.command == "dump-openapi":
78
78
  return _cmd_dump_openapi(args)
79
79
  parser.error(f"unknown command: {args.command}")
80
- return 2
81
80
 
82
81
 
83
82
  if __name__ == "__main__":
@@ -56,9 +56,9 @@ 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
60
59
  from fred_core.history.history_schema import ChatMessage
61
60
  from fred_core.kpi import KPIMiddleware
61
+ from fred_core.kpi.kpi_runtime_stage_metric import runtime_stage_timer
62
62
  from fred_core.kpi.kpi_writer_structures import KPIActor
63
63
  from fred_core.logs.audit_log import emit_audit_log
64
64
  from fred_core.logs.log_setup import log_setup
@@ -89,6 +89,7 @@ from fred_sdk.contracts.context import (
89
89
  PortableContext,
90
90
  PortableEnvironment,
91
91
  RuntimeContext,
92
+ TeamOperationRouteRule,
92
93
  )
93
94
  from fred_sdk.contracts.eval import EvalStep, EvalTrace
94
95
  from fred_sdk.contracts.execution import (
@@ -146,8 +147,13 @@ from fred_runtime.runtime_support.checkpoints import load_checkpoint
146
147
  from ..common.structures import AgentSettingsLike
147
148
  from ..integrations.inprocess_toolkit_registry import build_inprocess_toolkit
148
149
  from ..integrations.v2_runtime.adapters import (
150
+ AgentConfigAssetsAdapter,
149
151
  CompositeToolInvoker,
152
+ DocumentContentAdapter,
153
+ DocumentFolderAdapter,
150
154
  DocumentSearchAdapter,
155
+ DocumentSummarizeAdapter,
156
+ DocumentTreeAdapter,
151
157
  FredKnowledgeSearchToolInvoker,
152
158
  FredMcpToolProvider,
153
159
  FredWorkspaceFs,
@@ -734,6 +740,16 @@ def _build_runtime_services(
734
740
  binding=binding,
735
741
  settings=settings,
736
742
  )
743
+ # Companion document-access ports: tree listing + on-demand summarization,
744
+ # same private-binding doctrine as the search adapter.
745
+ document_tree = DocumentTreeAdapter(
746
+ binding=binding,
747
+ settings=settings,
748
+ )
749
+ document_summarize = DocumentSummarizeAdapter(
750
+ binding=binding,
751
+ settings=settings,
752
+ )
737
753
  tool_provider = FredMcpToolProvider(
738
754
  binding=binding,
739
755
  settings=settings,
@@ -784,6 +800,14 @@ def _build_runtime_services(
784
800
  checkpointer=runtime_config.checkpointer,
785
801
  agent_invoker=agent_invoker,
786
802
  document_search=document_search,
803
+ # #1903 capability ports: per-instance config assets (template fetch at
804
+ # tool time), image-document raw fetch, and folder listing. Same
805
+ # private-binding doctrine as document_search.
806
+ agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
807
+ document_content=DocumentContentAdapter(binding=binding, settings=settings),
808
+ document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
809
+ document_tree=document_tree,
810
+ document_summarize=document_summarize,
787
811
  )
788
812
 
789
813
 
@@ -945,6 +969,66 @@ class _McpCatalogResponse(BaseModel):
945
969
  servers: list[_McpCatalogEntry]
946
970
 
947
971
 
972
+ class _ModelCatalogEntry(BaseModel):
973
+ id: str
974
+ provider: str
975
+ name: str
976
+ description: str | None = None
977
+ profile_ids: list[str] = Field(default_factory=list)
978
+ """Every `models_catalog.yaml` profile_id sharing this entry's
979
+ (provider, name) — TEAM-ROUTING-POLICY-RFC.md §7.1's id-space
980
+ translation: a routing policy picks by profile_id (finer-grained than
981
+ this capability id), so control-plane needs a way back from profile_id
982
+ to capability id for write-time enablement validation, and the
983
+ team-settings picker needs the reverse (which profile_ids this enabled
984
+ capability actually offers). Declaration order in the source YAML,
985
+ first-seen first — same order `default_profile_by_capability` favors."""
986
+
987
+
988
+ class _ModelCatalogResponse(BaseModel):
989
+ models: list[_ModelCatalogEntry]
990
+
991
+
992
+ def _project_model_catalog_entries(catalog: Any) -> list[_ModelCatalogEntry]:
993
+ """One `_ModelCatalogEntry` per distinct (provider, name) pair in a
994
+ loaded `ModelCatalog` (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
995
+
996
+ Pulled out of `get_models_catalog` as a pure function so it is directly
997
+ unit-testable without a running app (`agent_app.py` has no existing
998
+ FastAPI TestClient harness — building one just for this route would be
999
+ disproportionate; this function is where the actual logic worth testing
1000
+ lives). `catalog` is typed `Any` here to avoid a module-level
1001
+ `..model_routing` import (see the route's own lazy import for why);
1002
+ callers pass a real `ModelCatalog`.
1003
+ """
1004
+ from fred_sdk.contracts.capability.manifest import model_capability_id
1005
+
1006
+ seen: dict[tuple[str, str], _ModelCatalogEntry] = {}
1007
+ for profile in catalog.profiles:
1008
+ provider = profile.model.provider
1009
+ name = profile.model.name
1010
+ # ModelProfile.validate_model (contracts.py) already guarantees both
1011
+ # are non-empty strings for every profile that exists — asserted
1012
+ # (not a skip/continue) so this narrows the type instead of hiding
1013
+ # an invariant violation the validator itself is supposed to prevent.
1014
+ assert provider and name, (
1015
+ f"ModelProfile {profile.profile_id!r} passed validation without provider/name"
1016
+ )
1017
+ key = (provider, name)
1018
+ existing = seen.get(key)
1019
+ if existing is None:
1020
+ seen[key] = _ModelCatalogEntry(
1021
+ id=model_capability_id(provider, name),
1022
+ provider=provider,
1023
+ name=name,
1024
+ description=profile.description,
1025
+ profile_ids=[profile.profile_id],
1026
+ )
1027
+ else:
1028
+ existing.profile_ids.append(profile.profile_id)
1029
+ return list(seen.values())
1030
+
1031
+
948
1032
  class _ResolvedAgentInstance(BaseModel):
949
1033
  agent_instance_id: str
950
1034
  template_agent_id: str
@@ -1098,7 +1182,9 @@ async def _resolve_agent_instance(
1098
1182
  registry: Mapping[str, ReActAgentDefinition | GraphAgentDefinition],
1099
1183
  access_token: str | None,
1100
1184
  control_plane_url: str | None,
1185
+ http_client: httpx.AsyncClient,
1101
1186
  team_id: str | None = None,
1187
+ request_id: str | None = None,
1102
1188
  ) -> _ResolvedExecutionTarget:
1103
1189
  """
1104
1190
  Resolve a direct or managed execution target into a concrete definition.
@@ -1173,9 +1259,17 @@ async def _resolve_agent_instance(
1173
1259
  f"{control_plane_url.rstrip('/')}/teams/{team_id}/agent-instances/"
1174
1260
  f"{request.agent_instance_id}/runtime"
1175
1261
  )
1176
- headers = {"Authorization": f"Bearer {access_token}"} if access_token else None
1177
- async with httpx.AsyncClient(timeout=10.0) as client:
1178
- response = await client.get(url, headers=headers)
1262
+ headers: dict[str, str] = {}
1263
+ if access_token:
1264
+ headers["Authorization"] = f"Bearer {access_token}"
1265
+ if request_id:
1266
+ # Correlates this pod-side call with the control-plane request it
1267
+ # triggers — both sides stamp the same id as `trace.trace_id` on their
1268
+ # `runtime.stage_latency_ms{runtime_stage=runtime_binding*}` event
1269
+ # (TURN-01 evidence gap: no way to join a turn to its binding call
1270
+ # besides timestamps).
1271
+ headers["X-Request-Id"] = request_id
1272
+ response = await http_client.get(url, headers=headers or None)
1179
1273
  if response.status_code == status.HTTP_404_NOT_FOUND:
1180
1274
  raise HTTPException(
1181
1275
  status_code=404, detail=response.text or "Unknown agent instance."
@@ -1260,12 +1354,13 @@ async def _authorize_execution_or_raise(
1260
1354
  - security disabled (no authenticated user) → skip (dev/local).
1261
1355
  - ReBAC engine absent or disabled (Noop) → skip (identity-only dev posture);
1262
1356
  the C3 profile guarantees an enabled engine in classified deployments.
1263
- - a personal space (`personal-<uid>`) is intrinsic ownership, not a stored
1264
- OpenFGA tuple: a human caller acting on their own canonical
1265
- `personal_team_id(authenticated_user.uid)` is authorized without an OpenFGA
1266
- call; any other `personal-*` id (another user's space, or the bare
1267
- `"personal"` alias) is explicitly denied never routed to OpenFGA (AUTHZ-05
1268
- item 8b watch item).
1357
+ - a personal space (`personal-<uid>`) is a real ReBAC team object (AUTHZ-08):
1358
+ the owner's `team_editor` tuple self-heals on first touch inside
1359
+ `RebacEngine.check_user_team_permission_or_raise`, and `add_relation`'s
1360
+ write-guard refuses any tuple naming a personal team except that one, so
1361
+ the plain `CAN_READ` check below already authorizes the owner and denies
1362
+ everyone else (another user's space, or the bare `"personal"` alias, for
1363
+ which no tuple is ever provisioned) — no special-casing needed here.
1269
1364
  - otherwise require the caller to hold `CAN_READ` on the requested team — the
1270
1365
  same relation the control-plane required before it would mint a grant. The
1271
1366
  team is caller-supplied but safe: OpenFGA only authorizes teams the user
@@ -1341,36 +1436,14 @@ async def _authorize_execution_or_raise(
1341
1436
  agent_instance_id=request.agent_instance_id,
1342
1437
  )
1343
1438
  return
1344
- if team_id == "personal" or is_personal_team_id(team_id):
1345
- # Personal spaces are intrinsic ownership (AUTHZ-05 item 8b): the id is
1346
- # derived from the JWT subject via `personal_team_id`, is never persisted
1347
- # as an OpenFGA tuple, and must never be resolved by OpenFGA. Only an
1348
- # exact match against the caller's own canonical id is authorized; any
1349
- # other personal-* id (another user's space) or the bare "personal"
1350
- # alias (ambiguous once ReBAC is active) is denied here, before OpenFGA
1351
- # ever sees it.
1352
- if team_id == personal_team_id(authenticated_user.uid):
1353
- _emit_audit_event(
1354
- container,
1355
- "info",
1356
- "personal_space_owner_authorized",
1357
- user_id=authenticated_user.uid,
1358
- team_id=team_id,
1359
- agent_instance_id=request.agent_instance_id,
1360
- )
1361
- return
1362
- _emit_audit_event(
1363
- container,
1364
- "warning",
1365
- "personal_space_denied",
1366
- user_id=authenticated_user.uid,
1367
- team_id=team_id,
1368
- agent_instance_id=request.agent_instance_id,
1369
- )
1370
- raise HTTPException(
1371
- status_code=status.HTTP_403_FORBIDDEN,
1372
- detail=f"user {authenticated_user.uid!r} is not authorized for team {team_id!r}",
1373
- )
1439
+ # Personal spaces (AUTHZ-08, supersedes AUTHZ-05 item 8b) are now real
1440
+ # ReBAC team objects: `RebacEngine.check_user_team_permission_or_raise`
1441
+ # self-heals the owner's own `team_editor` tuple on first touch and
1442
+ # `add_relation`'s write-guard refuses any other shape naming a personal
1443
+ # team, so the bare `check_user_team_permission_or_raise` call below
1444
+ # already authorizes the owner and denies everyone else no special-
1445
+ # casing needed here (the bare "personal" alias also denies normally,
1446
+ # since no tuple is ever provisioned for that literal string).
1374
1447
  try:
1375
1448
  await rebac.check_user_team_permission_or_raise(
1376
1449
  authenticated_user, TeamPermission.CAN_READ, team_id
@@ -1506,15 +1579,34 @@ async def _authorize_and_resolve(
1506
1579
  )
1507
1580
  await _validate_session_checkpoint_access(request)
1508
1581
  await _enforce_session_ownership(request, authenticated_user, container)
1509
- await _authorize_execution_or_raise(request, authenticated_user, container)
1582
+ async with runtime_stage_timer(container.get_kpi_writer(), "pod_authz"):
1583
+ await _authorize_execution_or_raise(request, authenticated_user, container)
1510
1584
  internal_req = _to_internal_request(request)
1511
- target = await _resolve_agent_instance(
1512
- request=internal_req,
1513
- registry=registry,
1514
- access_token=access_token,
1515
- control_plane_url=get_runtime_context().config.control_plane_url,
1516
- team_id=request.effective_team_id(),
1517
- )
1585
+ # Stamp the trusted service-agent verdict (never the caller-supplied
1586
+ # context) so per-tool-call re-authorization can mirror the bypass
1587
+ # `_authorize_execution_or_raise` already granted above (RFC EVAL-AUTH,
1588
+ # Solution A) instead of re-running a ReBAC check this identity was
1589
+ # never meant to satisfy. Overwritten unconditionally, both ways, so a
1590
+ # caller can't spoof it via a body-supplied `context.is_service_agent`.
1591
+ ctx = dict(internal_req.context or {})
1592
+ if authenticated_user is not None and is_service_agent(authenticated_user):
1593
+ ctx["is_service_agent"] = "true"
1594
+ else:
1595
+ ctx.pop("is_service_agent", None)
1596
+ internal_req.context = ctx
1597
+ binding_request_id = str(uuid4())
1598
+ async with runtime_stage_timer(
1599
+ container.get_kpi_writer(), "runtime_binding", trace_id=binding_request_id
1600
+ ):
1601
+ target = await _resolve_agent_instance(
1602
+ request=internal_req,
1603
+ registry=registry,
1604
+ access_token=access_token,
1605
+ control_plane_url=get_runtime_context().config.control_plane_url,
1606
+ http_client=container.get_control_plane_http_client(),
1607
+ team_id=request.effective_team_id(),
1608
+ request_id=binding_request_id,
1609
+ )
1518
1610
  _validate_resolved_team(request, target.team_id, container)
1519
1611
  return internal_req, target
1520
1612
 
@@ -1757,6 +1849,7 @@ async def _write_turn_history(
1757
1849
  call_id=payload["call_id"],
1758
1850
  content=payload.get("content", ""),
1759
1851
  ok=not payload.get("is_error", False),
1852
+ latency_ms=payload.get("latency_ms"),
1760
1853
  )
1761
1854
  )
1762
1855
  rank += 1
@@ -2163,6 +2256,7 @@ def _build_capability_save_services(
2163
2256
  user_id: str,
2164
2257
  team_id: str | None,
2165
2258
  access_token: str | None,
2259
+ agent_instance_id: str | None = None,
2166
2260
  ) -> RuntimeServices:
2167
2261
  """
2168
2262
  Minimal `RuntimeServices` for one capability save-time validation (#1974).
@@ -2170,8 +2264,11 @@ def _build_capability_save_services(
2170
2264
  Why this exists:
2171
2265
  - `validate_config` may store uploaded asset binaries through the KF-backed
2172
2266
  workspace port and keep only the storage keys in the stored config
2173
- (RFC §3.4, §3.8) — so the save path needs `workspace_fs`, bound to the
2174
- saving user's identity/team, but none of the execution-only services
2267
+ (RFC §3.4, §3.8) — so the save path needs `workspace_fs` plus the
2268
+ per-instance `agent_assets` store (#1903), bound to the saving user's
2269
+ identity/team, but none of the execution-only services
2270
+ - `document_folders` lets an asset-parsing capability resolve author folder
2271
+ strings against the agent's space at save time (#1903 image support)
2175
2272
  """
2176
2273
 
2177
2274
  request_id = str(uuid4())
@@ -2181,6 +2278,7 @@ def _build_capability_save_services(
2181
2278
  user_id=user_id,
2182
2279
  team_id=team_id,
2183
2280
  access_token=access_token,
2281
+ agent_instance_id=agent_instance_id,
2184
2282
  ),
2185
2283
  portable_context=PortableContext(
2186
2284
  request_id=request_id,
@@ -2196,7 +2294,9 @@ def _build_capability_save_services(
2196
2294
  )
2197
2295
  settings = _PodAgentSettings(id=actor, name=actor, team_id=team_id, tuning=None)
2198
2296
  return RuntimeServices(
2199
- workspace_fs=FredWorkspaceFs(binding=binding, settings=settings)
2297
+ workspace_fs=FredWorkspaceFs(binding=binding, settings=settings),
2298
+ agent_assets=AgentConfigAssetsAdapter(binding=binding, settings=settings),
2299
+ document_folders=DocumentFolderAdapter(binding=binding, settings=settings),
2200
2300
  )
2201
2301
 
2202
2302
 
@@ -2223,13 +2323,11 @@ def _effective_capability_ids(
2223
2323
  `default_mcp_servers` ids; a selected id that is a known-but-DISABLED
2224
2324
  catalog MCP server is dropped (the live tool provider skips it anyway —
2225
2325
  #1988 keeps that tolerance), while an id the pod knows nothing about stays
2226
- and fails loudly downstream. Non-ReAct templates carry no capabilities.
2227
- When the registry is absent the raw selection is returned unfiltered — the
2228
- caller decides whether that is an error.
2326
+ and fails loudly downstream. Resolved identically for ReAct and Graph
2327
+ agents. When the registry is absent the raw selection is returned
2328
+ unfiltered — the caller decides whether that is an error.
2229
2329
  """
2230
2330
 
2231
- if not isinstance(definition, ReActAgentDefinition):
2232
- return []
2233
2331
  selected = tuning.selected_capability_ids if tuning is not None else None
2234
2332
  if selected is None:
2235
2333
  selected = [ref.id for ref in definition.default_mcp_servers]
@@ -2322,23 +2420,20 @@ def _build_capability_block(
2322
2420
  Returns None when the agent selects no capabilities.
2323
2421
 
2324
2422
  MCP handling (#1978, #1988): an MCP-server capability delivers its catalog
2325
- `agent_instructions` as a prompt fragment. The effective selection mirrors
2423
+ `agent_instructions` as a prompt fragment via `middleware()`
2424
+ (`_McpInstructionsMiddleware`). The effective selection mirrors
2326
2425
  `_active_mcp_server_refs`: a `None` capability selection (template default)
2327
2426
  activates the template's `default_mcp_servers` as capabilities so their
2328
- instructions are delivered — otherwise a default-configured agent would
2329
- silently lose its non-negotiable grounding contract.
2427
+ instructions are delivered — otherwise a default ReAct agent would silently
2428
+ lose its non-negotiable grounding contract. This block is built identically
2429
+ for both agent kinds (CAPAB-02), but a Graph agent reads only `block.tools`
2430
+ — MCP tools reach it (a separate, already execution-model-agnostic path,
2431
+ `FredMcpToolProvider`), the `agent_instructions` prompt fragment does NOT
2432
+ (Graph never reads `block.middleware` at all). A Graph agent that needs an
2433
+ MCP server's grounding instructions must currently author them into its own
2434
+ system prompt — this is an explicit, known gap, not yet closed (CAPAB-02).
2330
2435
  """
2331
2436
 
2332
- if not isinstance(definition, ReActAgentDefinition):
2333
- # Capabilities (incl. MCP instruction fragments) are ReAct-only (RFC §5).
2334
- # A non-ReAct template selecting real capabilities is still a loud error.
2335
- if tuning is not None and tuning.selected_capability_ids:
2336
- raise CapabilityError(
2337
- "Capabilities are only supported on ReAct agents (RFC §5); "
2338
- f"template '{definition.agent_id}' is not one."
2339
- )
2340
- return None
2341
-
2342
2437
  selected = tuning.selected_capability_ids if tuning is not None else None
2343
2438
  capability_config = tuning.capability_config if tuning is not None else {}
2344
2439
  if capability_registry is None:
@@ -2359,6 +2454,49 @@ def _build_capability_block(
2359
2454
  effective = _effective_capability_ids(tuning, definition, capability_registry)
2360
2455
  if not effective:
2361
2456
  return None
2457
+ if isinstance(definition, GraphAgentDefinition):
2458
+ # CAPAB-02: a capability can declare itself ReAct-only
2459
+ # (`CapabilityManifest.execution_models`) when its runtime need is a
2460
+ # `middleware()` hook `tools()` cannot express (e.g. PPT filler's
2461
+ # dynamic per-turn tool schema). Selecting one on a Graph agent must
2462
+ # fail loudly here — never silently contribute zero tools (RFC §3.9
2463
+ # "never silently degrade").
2464
+ react_only = [
2465
+ cap_id
2466
+ for cap_id in effective
2467
+ if cap_id in capability_registry
2468
+ and "graph"
2469
+ not in capability_registry.capability(cap_id).manifest.execution_models
2470
+ ]
2471
+ if react_only:
2472
+ raise CapabilityError(
2473
+ f"Agent selects capabilities {react_only} which are ReAct-only "
2474
+ "(CapabilityManifest.execution_models) and cannot run on a "
2475
+ "Graph agent."
2476
+ )
2477
+ # CAPAB-02 stopgap: `CapabilityAgentBlock.hitl` is built (assembly.py)
2478
+ # but `GraphRuntime.invoke_runtime_tool` never consults it — a
2479
+ # capability's `HitlSpec` gates a ReAct tool call but not a Graph
2480
+ # one. No production capability declares an active `HitlSpec` today,
2481
+ # so refusing here costs nothing real yet; reconciling Graph's own
2482
+ # node-level pause/resume with the per-tool HITL gate is real design
2483
+ # work, deferred (see AGENT-CAPABILITY-RFC.md §3.9). Refusing loudly
2484
+ # keeps the RFC's "never silently degrade" guarantee intact in the meantime
2485
+ # — a capability with `HitlSpec`s that silently ran ungated on Graph
2486
+ # would be exactly the kind of governance gap this platform exists to
2487
+ # prevent.
2488
+ hitl_gated = [
2489
+ cap_id
2490
+ for cap_id in effective
2491
+ if cap_id in capability_registry
2492
+ and capability_registry.capability(cap_id).hitl_specs()
2493
+ ]
2494
+ if hitl_gated:
2495
+ raise CapabilityError(
2496
+ f"Agent selects capabilities {hitl_gated} which declare "
2497
+ "HitlSpec approval gates; Graph agents do not yet enforce "
2498
+ "capability HITL (CAPAB-02) and cannot run them."
2499
+ )
2362
2500
  contexts = build_capability_contexts(
2363
2501
  capability_registry,
2364
2502
  selected_capability_ids=effective,
@@ -2413,6 +2551,19 @@ async def _iterate_runtime_event_payloads(
2413
2551
  ctx = request.context or {}
2414
2552
  correlation_id = ctx.get("correlation_id", request_id)
2415
2553
  resolved_team_id = team_id or ctx.get("team_id")
2554
+ # kind="model" enforcement (OBSERV-02 v3, AGENT-CAPABILITY-RFC.md §8.7):
2555
+ # computed ONCE per turn, here — never inside model-routing resolution,
2556
+ # which runs multiple times per turn and must never itself make a ReBAC
2557
+ # call. No team context (agent-to-agent invocation) → no restriction,
2558
+ # the same posture _authorize_execution_or_raise already applies to a
2559
+ # teamless request.
2560
+ usable_model_ids: tuple[str, ...] | None = None
2561
+ if resolved_team_id is not None:
2562
+ from ..model_routing import usable_model_capability_ids
2563
+
2564
+ rebac = get_runtime_context().config.rebac_engine
2565
+ ids = await usable_model_capability_ids(rebac, resolved_team_id)
2566
+ usable_model_ids = tuple(ids) if ids is not None else None
2416
2567
  execution_action = ctx.get("execution_action") or (
2417
2568
  ExecutionGrantAction.RESUME.value
2418
2569
  if request.resume_payload is not None
@@ -2440,6 +2591,7 @@ async def _iterate_runtime_event_payloads(
2440
2591
  "checkpoint_id": resolved_checkpoint_id,
2441
2592
  "execution_action": execution_action,
2442
2593
  "exchange_id": exchange_id,
2594
+ "is_service_agent": ctx.get("is_service_agent"),
2443
2595
  }.items()
2444
2596
  if isinstance(value, str) and value
2445
2597
  },
@@ -2480,11 +2632,26 @@ async def _iterate_runtime_event_payloads(
2480
2632
  # When the final file is deleted this is absent, so the per-turn runtime
2481
2633
  # notice disappears without leaving a checkpointed system message behind.
2482
2634
  attachments_markdown=ctx.get("attachments_markdown"),
2635
+ # Team routing policy snapshot (TEAM-ROUTING-POLICY-RFC.md §3/§8):
2636
+ # control-plane resolves it once at prepare-execution and the frontend
2637
+ # forwards it unchanged, same channel as context_prompt_text above — but
2638
+ # it was also silently dropped here, so resolve_team_override in
2639
+ # fred_runtime.model_routing.provider always saw None and no team's
2640
+ # routing policy ever took effect on a real chat turn. ctx already holds
2641
+ # the flat dict from to_legacy_context()'s model_dump(exclude_none=True);
2642
+ # operation_route_rules needs re-validating back into typed rows.
2643
+ chat_default_profile_id=ctx.get("chat_default_profile_id"),
2644
+ operation_route_rules=[
2645
+ TeamOperationRouteRule.model_validate(rule)
2646
+ for rule in (ctx.get("operation_route_rules") or [])
2647
+ ]
2648
+ or None,
2483
2649
  )
2484
2650
 
2485
2651
  binding = BoundRuntimeContext(
2486
2652
  runtime_context=runtime_context,
2487
2653
  portable_context=portable_context,
2654
+ usable_model_ids=usable_model_ids,
2488
2655
  )
2489
2656
 
2490
2657
  services = _build_runtime_services(
@@ -2540,6 +2707,7 @@ async def _iterate_runtime_event_payloads(
2540
2707
  runtime = GraphRuntime(
2541
2708
  definition=definition,
2542
2709
  services=services,
2710
+ capability_block=capability_block,
2543
2711
  )
2544
2712
  runtime.bind(binding)
2545
2713
  await runtime.activate()
@@ -2806,7 +2974,7 @@ def _build_agent_router(
2806
2974
  """
2807
2975
 
2808
2976
  capability_registry = _capability_registry_of(http_request)
2809
- available_capabilities = (
2977
+ all_capability_entries = (
2810
2978
  [
2811
2979
  CapabilityCatalogEntry.from_manifest(
2812
2980
  capability_registry.capability(cap_id).manifest,
@@ -2826,7 +2994,21 @@ def _build_agent_router(
2826
2994
  kind=definition.execution_category,
2827
2995
  default_tuning=_definition_to_agent_tuning(definition),
2828
2996
  available_mcp_servers=_available_mcp_servers_for_definition(definition),
2829
- available_capabilities=available_capabilities,
2997
+ # CAPAB-02: a capability declared `execution_models=("react",)`
2998
+ # must not even be offered for selection on a Graph template —
2999
+ # `_build_capability_block` would refuse it loudly at save/run
3000
+ # time regardless, but a picker that lists it first invites the
3001
+ # exact "select it, save it, discover the incompatibility at
3002
+ # first launch" flow the loud refusal exists to prevent.
3003
+ available_capabilities=(
3004
+ all_capability_entries
3005
+ if not isinstance(definition, GraphAgentDefinition)
3006
+ else [
3007
+ entry
3008
+ for entry in all_capability_entries
3009
+ if "graph" in entry.execution_models
3010
+ ]
3011
+ ),
2830
3012
  default_capability_ids=[
2831
3013
  ref.id for ref in definition.default_mcp_servers
2832
3014
  ],
@@ -2869,6 +3051,42 @@ def _build_agent_router(
2869
3051
  ]
2870
3052
  )
2871
3053
 
3054
+ @router.get("/models-catalog")
3055
+ async def get_models_catalog() -> _ModelCatalogResponse:
3056
+ """
3057
+ Return this pod's routable models, one entry per distinct
3058
+ (provider, name) pair (OBSERV-02 v3, `AGENT-CAPABILITY-RFC.md` §8.7).
3059
+
3060
+ Why this endpoint exists:
3061
+ - control-plane's capability catalog aggregation
3062
+ (`aggregate_capability_catalog`) has no other way to learn what
3063
+ models this pod can route to — `models_catalog.yaml` is loaded
3064
+ only here, for routing, with no prior control-plane consumer.
3065
+ - one entry per (provider, name), not per catalog profile: a model
3066
+ used for both the `chat` and `language` routing capability is one
3067
+ admin enablement decision, not two.
3068
+
3069
+ Re-reads `models_catalog.yaml` fresh on every call (same file
3070
+ `_build_chat_model_factory` loaded once at boot) rather than
3071
+ caching the parsed catalog — this route is polled rarely (admin
3072
+ catalog refresh), never per-turn, so the extra local file read is
3073
+ immaterial and keeps this always consistent with whatever the pod
3074
+ would actually resolve, with no separate cache to go stale.
3075
+
3076
+ How to use it:
3077
+ - call from control-plane's `_model_capabilities_for_source`
3078
+
3079
+ Example:
3080
+ - `GET /fred/agents/v2/agents/models-catalog`
3081
+ """
3082
+ from ..model_routing import load_model_catalog
3083
+
3084
+ catalog_path = get_runtime_context().config.models_catalog_path
3085
+ if not catalog_path:
3086
+ return _ModelCatalogResponse(models=[])
3087
+ catalog = load_model_catalog(catalog_path)
3088
+ return _ModelCatalogResponse(models=_project_model_catalog_entries(catalog))
3089
+
2872
3090
  @router.post("/capabilities/{capability_id}/validate-config")
2873
3091
  async def validate_capability_config(
2874
3092
  capability_id: str,
@@ -2979,6 +3197,10 @@ def _build_agent_router(
2979
3197
  user_id=(caller.uid if caller is not None else None) or "anonymous",
2980
3198
  team_id=team_id or None,
2981
3199
  access_token=access_token,
3200
+ agent_instance_id=(
3201
+ form_instance_id if isinstance(form_instance_id, str) else None
3202
+ )
3203
+ or None,
2982
3204
  ),
2983
3205
  )
2984
3206
  try:
@@ -3806,12 +4028,13 @@ def create_agent_app(
3806
4028
  # Boot order (must be preserved — each step depends on the previous):
3807
4029
  # 1. log_setup — formatter/handlers ready for all subsequent logs
3808
4030
  # 2. initialize_kpi_writer — needed by bootstrap_observability
3809
- # 3. bootstrap_observabilityglobal tracer + metrics provider
3810
- # 4. attach_pod_containercontainer in app.state before any request
3811
- # 5. initialize_sql async, may take time
3812
- # 6. start_metrics_exporter prometheus thread, after KPI writer exists
3813
- # 7. start_kpi_tasks asyncio tasks, after SQL engine is known
3814
- # 8. set_runtime_context wires all built parts into the global config
4031
+ # 3. initialize_control_plane_clientsync, no network until first call
4032
+ # 4. bootstrap_observabilityglobal tracer + metrics provider
4033
+ # 5. attach_pod_container container in app.state before any request
4034
+ # 6. initialize_sql async, may take time
4035
+ # 7. start_metrics_exporter prometheus thread, after KPI writer exists
4036
+ # 8. start_kpi_tasks asyncio tasks, after SQL engine is known
4037
+ # 9. set_runtime_context — wires all built parts into the global config
3815
4038
  log_setup(
3816
4039
  service_name=config.app.name,
3817
4040
  log_level=config.app.log_level,
@@ -3822,6 +4045,7 @@ def create_agent_app(
3822
4045
  )
3823
4046
  container = build_pod_container(config)
3824
4047
  container.initialize_kpi_writer()
4048
+ container.initialize_control_plane_client()
3825
4049
  bootstrap_observability(
3826
4050
  config.observability, kpi_writer=container.get_kpi_writer()
3827
4051
  )
@@ -3839,7 +4063,11 @@ def create_agent_app(
3839
4063
  # every execution against OpenFGA; a disabled/Noop engine (dev) means
3840
4064
  # identity-only. Safe in all modes — the factory returns a Noop with a
3841
4065
  # KeycloackDisabled admin client when user/m2m auth is off.
3842
- rebac_engine = rebac_factory(security) if security is not None else None
4066
+ rebac_engine = (
4067
+ rebac_factory(security, kpi_writer=container.get_kpi_writer())
4068
+ if security is not None
4069
+ else None
4070
+ )
3843
4071
  chat_factory = _build_chat_model_factory(config)
3844
4072
  await container.initialize_sql()
3845
4073
  container.start_metrics_exporter()
@@ -3860,6 +4088,7 @@ def create_agent_app(
3860
4088
  checkpointer=checkpointer,
3861
4089
  history_store=history_store,
3862
4090
  mcp_configuration=config.get_mcp_configuration(),
4091
+ models_catalog_path=config.get_models_catalog_path(),
3863
4092
  inprocess_toolkit_factory=build_inprocess_toolkit,
3864
4093
  control_plane_url=config.platform.control_plane_url,
3865
4094
  rebac_engine=rebac_engine,