fast-agent-mcp 0.4.7__py3-none-any.whl

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 (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,474 @@
1
+ """
2
+ ACP Tool Call Permissions
3
+
4
+ Provides a permission handler that requests tool execution permission from the ACP client.
5
+ This follows the same pattern as elicitation handlers but for tool execution authorization.
6
+
7
+ Key features:
8
+ - Requests user permission before tool execution via ACP session/request_permission
9
+ - Supports persistent permissions (allow_always, reject_always) stored in .fast-agent/auths.md
10
+ - Fail-safe: defaults to DENY on any error
11
+ - In-memory caching for remembered permissions within a session
12
+ """
13
+
14
+ import asyncio
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Protocol, runtime_checkable
18
+
19
+ from acp.schema import (
20
+ PermissionOption,
21
+ ToolCallProgress,
22
+ ToolCallUpdate,
23
+ ToolKind,
24
+ )
25
+
26
+ from fast_agent.acp.permission_store import PermissionDecision, PermissionResult, PermissionStore
27
+ from fast_agent.core.logging.logger import get_logger
28
+
29
+ if TYPE_CHECKING:
30
+ from acp import AgentSideConnection
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class ToolPermissionRequest:
37
+ """Request for tool execution permission."""
38
+
39
+ tool_name: str
40
+ server_name: str
41
+ arguments: dict[str, Any] | None
42
+ tool_call_id: str | None = None
43
+
44
+
45
+ # Type for permission handler callbacks
46
+ ToolPermissionHandlerT = Callable[[ToolPermissionRequest], Awaitable[PermissionResult]]
47
+
48
+
49
+ @runtime_checkable
50
+ class ToolPermissionChecker(Protocol):
51
+ """
52
+ Protocol for checking tool execution permissions.
53
+
54
+ This allows permission checking to be injected into the MCP aggregator
55
+ without tight coupling to ACP.
56
+ """
57
+
58
+ async def check_permission(
59
+ self,
60
+ tool_name: str,
61
+ server_name: str,
62
+ arguments: dict[str, Any] | None = None,
63
+ tool_call_id: str | None = None,
64
+ ) -> PermissionResult:
65
+ """
66
+ Check if tool execution is permitted.
67
+
68
+ Args:
69
+ tool_name: Name of the tool to execute
70
+ server_name: Name of the MCP server providing the tool
71
+ arguments: Tool arguments
72
+ tool_call_id: Optional tool call ID for tracking
73
+
74
+ Returns:
75
+ PermissionResult indicating whether execution is allowed
76
+ """
77
+ ...
78
+
79
+
80
+ def _infer_tool_kind(tool_name: str, arguments: dict[str, Any] | None = None) -> ToolKind:
81
+ """
82
+ Infer the tool kind from the tool name and arguments.
83
+
84
+ Args:
85
+ tool_name: Name of the tool being called
86
+ arguments: Tool arguments
87
+
88
+ Returns:
89
+ The inferred ToolKind
90
+ """
91
+ name_lower = tool_name.lower()
92
+
93
+ # Common patterns for tool categorization
94
+ if any(word in name_lower for word in ["read", "get", "fetch", "list", "show", "cat"]):
95
+ return "read"
96
+ elif any(
97
+ word in name_lower for word in ["write", "edit", "update", "modify", "patch", "create"]
98
+ ):
99
+ return "edit"
100
+ elif any(word in name_lower for word in ["delete", "remove", "clear", "clean", "rm"]):
101
+ return "delete"
102
+ elif any(word in name_lower for word in ["move", "rename", "mv", "copy", "cp"]):
103
+ return "move"
104
+ elif any(word in name_lower for word in ["search", "find", "query", "grep", "locate"]):
105
+ return "search"
106
+ elif any(word in name_lower for word in ["execute", "run", "exec", "command", "bash", "shell"]):
107
+ return "execute"
108
+ elif any(word in name_lower for word in ["think", "plan", "reason", "analyze"]):
109
+ return "think"
110
+ elif any(word in name_lower for word in ["fetch", "download", "http", "request", "curl"]):
111
+ return "fetch"
112
+
113
+ return "other"
114
+
115
+
116
+ class ACPToolPermissionManager:
117
+ """
118
+ Manages tool execution permission requests via ACP.
119
+
120
+ This class provides a handler that can be used to request permission
121
+ from the ACP client before executing tools. It implements the
122
+ ToolPermissionChecker protocol for integration with the MCP aggregator.
123
+
124
+ Features:
125
+ - Checks persistent permissions from PermissionStore first
126
+ - Falls back to ACP client permission request
127
+ - Caches session-level permissions in memory
128
+ - Fail-safe: defaults to DENY on any error
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ connection: "AgentSideConnection",
134
+ session_id: str,
135
+ store: PermissionStore | None = None,
136
+ cwd: str | Path | None = None,
137
+ ) -> None:
138
+ """
139
+ Initialize the permission manager.
140
+
141
+ Args:
142
+ connection: The ACP connection to send permission requests on
143
+ session_id: The ACP session ID
144
+ store: Optional PermissionStore for persistence (created if not provided)
145
+ cwd: Working directory for the store (only used if store not provided)
146
+ """
147
+ self._connection = connection
148
+ self._session_id = session_id
149
+ self._store = store or PermissionStore(cwd=cwd)
150
+ # In-memory cache for session-level permissions (cleared on session end)
151
+ self._session_cache: dict[str, bool] = {}
152
+ self._lock = asyncio.Lock()
153
+
154
+ def _get_permission_key(self, tool_name: str, server_name: str) -> str:
155
+ """Get a unique key for remembering permissions."""
156
+ return f"{server_name}/{tool_name}"
157
+
158
+ async def check_permission(
159
+ self,
160
+ tool_name: str,
161
+ server_name: str,
162
+ arguments: dict[str, Any] | None = None,
163
+ tool_call_id: str | None = None,
164
+ ) -> PermissionResult:
165
+ """
166
+ Check if tool execution is permitted.
167
+
168
+ Order of checks:
169
+ 1. Session-level cache (for allow_once/reject_once remembered within session)
170
+ 2. Persistent store (for allow_always/reject_always)
171
+ 3. ACP client permission request
172
+
173
+ Args:
174
+ tool_name: Name of the tool to execute
175
+ server_name: Name of the MCP server providing the tool
176
+ arguments: Tool arguments
177
+ tool_call_id: Optional tool call ID for tracking
178
+
179
+ Returns:
180
+ PermissionResult indicating whether execution is allowed
181
+ """
182
+ permission_key = self._get_permission_key(tool_name, server_name)
183
+
184
+ try:
185
+ # 1. Check session-level cache
186
+ async with self._lock:
187
+ if permission_key in self._session_cache:
188
+ allowed = self._session_cache[permission_key]
189
+ logger.debug(
190
+ f"Using session-cached permission for {permission_key}: {allowed}",
191
+ name="acp_tool_permission_session_cache",
192
+ )
193
+ return PermissionResult(allowed=allowed, remember=True)
194
+
195
+ # 2. Check persistent store
196
+ stored_decision = await self._store.get(server_name, tool_name)
197
+ if stored_decision is not None:
198
+ allowed = stored_decision == PermissionDecision.ALLOW_ALWAYS
199
+ logger.debug(
200
+ f"Using stored permission for {permission_key}: {stored_decision.value}",
201
+ name="acp_tool_permission_stored",
202
+ )
203
+ # Cache in session for faster subsequent lookups
204
+ async with self._lock:
205
+ self._session_cache[permission_key] = allowed
206
+ return PermissionResult(allowed=allowed, remember=True)
207
+
208
+ # 3. Request permission from ACP client
209
+ return await self._request_permission_from_client(
210
+ tool_name=tool_name,
211
+ server_name=server_name,
212
+ arguments=arguments,
213
+ tool_call_id=tool_call_id,
214
+ permission_key=permission_key,
215
+ )
216
+
217
+ except Exception as e:
218
+ logger.error(
219
+ f"Error checking tool permission: {e}",
220
+ name="acp_tool_permission_error",
221
+ exc_info=True,
222
+ )
223
+ # FAIL-SAFE: Default to DENY on any error
224
+ return PermissionResult(allowed=False, remember=False)
225
+
226
+ async def _request_permission_from_client(
227
+ self,
228
+ tool_name: str,
229
+ server_name: str,
230
+ arguments: dict[str, Any] | None,
231
+ tool_call_id: str | None,
232
+ permission_key: str,
233
+ ) -> PermissionResult:
234
+ """
235
+ Request permission from the ACP client.
236
+
237
+ Args:
238
+ tool_name: Name of the tool
239
+ server_name: Name of the server
240
+ arguments: Tool arguments
241
+ tool_call_id: Tool call ID
242
+ permission_key: Cache key for this tool
243
+
244
+ Returns:
245
+ PermissionResult from the client
246
+ """
247
+ # Create descriptive title with argument summary
248
+ title = f"{server_name}/{tool_name}"
249
+ if arguments:
250
+ # Include key argument info in title for user context
251
+ arg_str = ", ".join(f"{k}={v}" for k, v in list(arguments.items())[:2])
252
+ if len(arg_str) > 50:
253
+ arg_str = arg_str[:47] + "..."
254
+ title = f"{title}({arg_str})"
255
+
256
+ # If we have an ACP toolCallId already (e.g. from streaming tool notifications),
257
+ # proactively update the tool call title so the client UI matches the permission prompt.
258
+ if tool_call_id and len(tool_call_id) == 32:
259
+ lowered = tool_call_id.lower()
260
+ if all(ch in "0123456789abcdef" for ch in lowered):
261
+ try:
262
+ await self._connection.session_update(
263
+ session_id=self._session_id,
264
+ update=ToolCallProgress(
265
+ tool_call_id=tool_call_id,
266
+ title=title,
267
+ status="pending",
268
+ session_update="tool_call_update",
269
+ ),
270
+ )
271
+ except Exception:
272
+ pass
273
+
274
+ # Create ToolCallUpdate object per ACP spec with raw_input for full argument visibility
275
+ tool_kind = _infer_tool_kind(tool_name, arguments)
276
+ tool_call = ToolCallUpdate(
277
+ tool_call_id=tool_call_id or "pending",
278
+ title=title,
279
+ kind=tool_kind,
280
+ status="pending",
281
+ raw_input=arguments, # Include full arguments so client can display them
282
+ )
283
+
284
+ # Create permission request with options
285
+ options = [
286
+ PermissionOption(
287
+ option_id="allow_once",
288
+ kind="allow_once",
289
+ name="Allow Once",
290
+ ),
291
+ PermissionOption(
292
+ option_id="allow_always",
293
+ kind="allow_always",
294
+ name="Always Allow",
295
+ ),
296
+ PermissionOption(
297
+ option_id="reject_once",
298
+ kind="reject_once",
299
+ name="Reject Once",
300
+ ),
301
+ PermissionOption(
302
+ option_id="reject_always",
303
+ kind="reject_always",
304
+ name="Never Allow",
305
+ ),
306
+ ]
307
+
308
+ try:
309
+ logger.info(
310
+ f"Requesting permission for {permission_key}",
311
+ name="acp_tool_permission_request",
312
+ tool_name=tool_name,
313
+ server_name=server_name,
314
+ )
315
+
316
+ # Send permission request to client using flattened parameters
317
+ response = await self._connection.request_permission(
318
+ options=options,
319
+ session_id=self._session_id,
320
+ tool_call=tool_call,
321
+ )
322
+
323
+ # Handle response
324
+ return await self._handle_permission_response(
325
+ response, permission_key, server_name, tool_name
326
+ )
327
+
328
+ except Exception as e:
329
+ logger.error(
330
+ f"Error requesting tool permission from client: {e}",
331
+ name="acp_tool_permission_request_error",
332
+ exc_info=True,
333
+ )
334
+ # FAIL-SAFE: Default to DENY on any error
335
+ return PermissionResult(allowed=False, remember=False)
336
+
337
+ async def _handle_permission_response(
338
+ self,
339
+ response: Any,
340
+ permission_key: str,
341
+ server_name: str,
342
+ tool_name: str,
343
+ ) -> PermissionResult:
344
+ """
345
+ Handle the permission response from the client.
346
+
347
+ Args:
348
+ response: The response from requestPermission
349
+ permission_key: Cache key
350
+ server_name: Server name
351
+ tool_name: Tool name
352
+
353
+ Returns:
354
+ PermissionResult based on client response
355
+ """
356
+ outcome = response.outcome
357
+ if not hasattr(outcome, "outcome"):
358
+ logger.warning(
359
+ f"Unknown permission response format for {permission_key}, defaulting to reject",
360
+ name="acp_tool_permission_unknown_format",
361
+ )
362
+ return PermissionResult(allowed=False, remember=False)
363
+
364
+ outcome_type = outcome.outcome
365
+
366
+ if outcome_type == "cancelled":
367
+ logger.info(
368
+ f"Permission request cancelled for {permission_key}",
369
+ name="acp_tool_permission_cancelled",
370
+ )
371
+ return PermissionResult.cancelled()
372
+
373
+ if outcome_type == "selected":
374
+ option_id = getattr(outcome, "optionId", None)
375
+
376
+ if option_id == "allow_once":
377
+ logger.info(
378
+ f"Permission granted once for {permission_key}",
379
+ name="acp_tool_permission_allow_once",
380
+ )
381
+ return PermissionResult.allow_once()
382
+
383
+ elif option_id == "allow_always":
384
+ # Store in persistent store
385
+ await self._store.set(server_name, tool_name, PermissionDecision.ALLOW_ALWAYS)
386
+ # Also cache in session
387
+ async with self._lock:
388
+ self._session_cache[permission_key] = True
389
+ logger.info(
390
+ f"Permission granted always for {permission_key}",
391
+ name="acp_tool_permission_allow_always",
392
+ )
393
+ return PermissionResult.allow_always()
394
+
395
+ elif option_id == "reject_once":
396
+ logger.info(
397
+ f"Permission rejected once for {permission_key}",
398
+ name="acp_tool_permission_reject_once",
399
+ )
400
+ return PermissionResult.reject_once()
401
+
402
+ elif option_id == "reject_always":
403
+ # Store in persistent store
404
+ await self._store.set(server_name, tool_name, PermissionDecision.REJECT_ALWAYS)
405
+ # Also cache in session
406
+ async with self._lock:
407
+ self._session_cache[permission_key] = False
408
+ logger.info(
409
+ f"Permission rejected always for {permission_key}",
410
+ name="acp_tool_permission_reject_always",
411
+ )
412
+ return PermissionResult.reject_always()
413
+
414
+ # Unknown response type - FAIL-SAFE: DENY
415
+ logger.warning(
416
+ f"Unknown permission option for {permission_key}, defaulting to reject",
417
+ name="acp_tool_permission_unknown_option",
418
+ )
419
+ return PermissionResult(allowed=False, remember=False)
420
+
421
+ async def clear_session_cache(self) -> None:
422
+ """Clear the session-level permission cache."""
423
+ async with self._lock:
424
+ self._session_cache.clear()
425
+ logger.debug(
426
+ "Cleared session permission cache",
427
+ name="acp_tool_permission_cache_cleared",
428
+ )
429
+
430
+
431
+ class NoOpToolPermissionChecker:
432
+ """
433
+ No-op permission checker that always allows tool execution.
434
+
435
+ Used when --no-permissions flag is set or when not running in ACP mode.
436
+ """
437
+
438
+ async def check_permission(
439
+ self,
440
+ tool_name: str,
441
+ server_name: str,
442
+ arguments: dict[str, Any] | None = None,
443
+ tool_call_id: str | None = None,
444
+ ) -> PermissionResult:
445
+ """Always allows tool execution."""
446
+ return PermissionResult.allow_once()
447
+
448
+
449
+ def create_acp_permission_handler(
450
+ permission_manager: ACPToolPermissionManager,
451
+ ) -> ToolPermissionHandlerT:
452
+ """
453
+ Create a tool permission handler for ACP integration.
454
+
455
+ This creates a handler that can be injected into the tool execution
456
+ pipeline to request permission before executing tools.
457
+
458
+ Args:
459
+ permission_manager: The ACPToolPermissionManager instance
460
+
461
+ Returns:
462
+ A permission handler function
463
+ """
464
+
465
+ async def handler(request: ToolPermissionRequest) -> PermissionResult:
466
+ """Handle tool permission request."""
467
+ return await permission_manager.check_permission(
468
+ tool_name=request.tool_name,
469
+ server_name=request.server_name,
470
+ arguments=request.arguments,
471
+ tool_call_id=request.tool_call_id,
472
+ )
473
+
474
+ return handler