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,427 @@
1
+ """
2
+ ACPFilesystemRuntime - Read and write text files via ACP filesystem support.
3
+
4
+ This runtime allows FastAgent to read and write files through the ACP client's filesystem
5
+ capabilities when available (e.g., in Zed editor). This provides better integration and
6
+ security compared to direct file system access.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from mcp.types import CallToolResult, Tool
12
+
13
+ from fast_agent.core.logging.logger import get_logger
14
+ from fast_agent.mcp.helpers.content_helpers import text_content
15
+
16
+ if TYPE_CHECKING:
17
+ from acp import AgentSideConnection
18
+ from acp.schema import ReadTextFileResponse
19
+
20
+ from fast_agent.mcp.tool_execution_handler import ToolExecutionHandler
21
+ from fast_agent.mcp.tool_permission_handler import ToolPermissionHandler
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class ACPFilesystemRuntime:
27
+ """
28
+ Provides file reading and writing through ACP filesystem support.
29
+
30
+ This runtime implements the "read_text_file" and "write_text_file" tools by delegating
31
+ to the ACP client's filesystem capabilities. The client (e.g., Zed editor) handles
32
+ file access and permissions, providing a secure sandboxed environment.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ connection: "AgentSideConnection",
38
+ session_id: str,
39
+ activation_reason: str,
40
+ logger_instance=None,
41
+ enable_read: bool = True,
42
+ enable_write: bool = True,
43
+ tool_handler: "ToolExecutionHandler | None" = None,
44
+ permission_handler: "ToolPermissionHandler | None" = None,
45
+ ):
46
+ """
47
+ Initialize the ACP filesystem runtime.
48
+
49
+ Args:
50
+ connection: The ACP connection to use for filesystem operations
51
+ session_id: The ACP session ID for this runtime
52
+ activation_reason: Human-readable reason for activation
53
+ logger_instance: Optional logger instance
54
+ enable_read: Whether to enable the read_text_file tool
55
+ enable_write: Whether to enable the write_text_file tool
56
+ tool_handler: Optional tool execution handler for telemetry
57
+ permission_handler: Optional permission handler for tool execution authorization
58
+ """
59
+ self.connection = connection
60
+ self.session_id = session_id
61
+ self.activation_reason = activation_reason
62
+ self.logger = logger_instance or logger
63
+ self._enable_read = enable_read
64
+ self._enable_write = enable_write
65
+ self._tool_handler = tool_handler
66
+ self._permission_handler = permission_handler
67
+
68
+ # Tool definition for reading text files
69
+ self._read_tool = Tool(
70
+ name="read_text_file",
71
+ description="Read content from a text file. Returns the file contents as a string. ",
72
+ inputSchema={
73
+ "type": "object",
74
+ "properties": {
75
+ "path": {
76
+ "type": "string",
77
+ "description": "Absolute path to the file to read.",
78
+ },
79
+ "line": {
80
+ "type": "integer",
81
+ "description": "Optional line number to start reading from (1-based).",
82
+ "minimum": 1,
83
+ },
84
+ "limit": {
85
+ "type": "integer",
86
+ "description": "Optional maximum number of lines to read.",
87
+ "minimum": 1,
88
+ },
89
+ },
90
+ "required": ["path"],
91
+ "additionalProperties": False,
92
+ },
93
+ )
94
+
95
+ # Tool definition for writing text files
96
+ self._write_tool = Tool(
97
+ name="write_text_file",
98
+ description="Write content to a text file. Creates or overwrites the file. ",
99
+ inputSchema={
100
+ "type": "object",
101
+ "properties": {
102
+ "path": {
103
+ "type": "string",
104
+ "description": "Absolute path to the file to write.",
105
+ },
106
+ "content": {
107
+ "type": "string",
108
+ "description": "The text content to write to the file.",
109
+ },
110
+ },
111
+ "required": ["path", "content"],
112
+ "additionalProperties": False,
113
+ },
114
+ )
115
+
116
+ self.logger.info(
117
+ "ACPFilesystemRuntime initialized",
118
+ session_id=session_id,
119
+ reason=activation_reason,
120
+ )
121
+
122
+ @property
123
+ def read_tool(self) -> Tool:
124
+ """Get the read_text_file tool definition."""
125
+ return self._read_tool
126
+
127
+ @property
128
+ def write_tool(self) -> Tool:
129
+ """Get the write_text_file tool definition."""
130
+ return self._write_tool
131
+
132
+ @property
133
+ def tools(self) -> list[Tool]:
134
+ """Get all enabled filesystem tools."""
135
+ tools = []
136
+ if self._enable_read:
137
+ tools.append(self._read_tool)
138
+ if self._enable_write:
139
+ tools.append(self._write_tool)
140
+ return tools
141
+
142
+ async def read_text_file(
143
+ self, arguments: dict[str, Any], tool_use_id: str | None = None
144
+ ) -> CallToolResult:
145
+ """
146
+ Read a text file using ACP filesystem support.
147
+
148
+ Args:
149
+ arguments: Tool arguments containing 'path' and optionally 'line' and 'limit'
150
+ tool_use_id: LLM's tool use ID (for matching with stream events)
151
+
152
+ Returns:
153
+ CallToolResult with file contents
154
+ """
155
+ # Validate arguments
156
+ if not isinstance(arguments, dict):
157
+ return CallToolResult(
158
+ content=[text_content("Error: arguments must be a dict")],
159
+ isError=True,
160
+ )
161
+
162
+ path = arguments.get("path")
163
+ if not path or not isinstance(path, str):
164
+ return CallToolResult(
165
+ content=[text_content("Error: 'path' argument is required and must be a string")],
166
+ isError=True,
167
+ )
168
+
169
+ self.logger.info(
170
+ "Reading file via ACP filesystem",
171
+ session_id=self.session_id,
172
+ path=path,
173
+ )
174
+
175
+ # Check permission before execution
176
+ if self._permission_handler:
177
+ try:
178
+ permission_result = await self._permission_handler.check_permission(
179
+ tool_name="read_text_file",
180
+ server_name="acp_filesystem",
181
+ arguments=arguments,
182
+ tool_use_id=tool_use_id,
183
+ )
184
+ if not permission_result.allowed:
185
+ error_msg = permission_result.error_message or (
186
+ f"Permission denied for reading file: {path}"
187
+ )
188
+ self.logger.info(
189
+ "File read denied by permission handler",
190
+ data={
191
+ "path": path,
192
+ "cancelled": permission_result.is_cancelled,
193
+ },
194
+ )
195
+ return CallToolResult(
196
+ content=[text_content(error_msg)],
197
+ isError=True,
198
+ )
199
+ except Exception as e:
200
+ self.logger.error(f"Error checking file read permission: {e}", exc_info=True)
201
+ # Fail-safe: deny on permission check error
202
+ return CallToolResult(
203
+ content=[text_content(f"Permission check failed: {e}")],
204
+ isError=True,
205
+ )
206
+
207
+ # Notify tool handler that execution is starting
208
+ tool_call_id = None
209
+ if self._tool_handler:
210
+ try:
211
+ tool_call_id = await self._tool_handler.on_tool_start(
212
+ "read_text_file", "acp_filesystem", arguments, tool_use_id
213
+ )
214
+ except Exception as e:
215
+ self.logger.error(f"Error in tool start handler: {e}", exc_info=True)
216
+
217
+ try:
218
+ # Send request using the proper ACP method with flattened parameters
219
+ response: ReadTextFileResponse = await self.connection.read_text_file(
220
+ path=path,
221
+ session_id=self.session_id,
222
+ line=arguments.get("line"),
223
+ limit=arguments.get("limit"),
224
+ )
225
+ content = response.content
226
+
227
+ self.logger.info(
228
+ "File read completed",
229
+ session_id=self.session_id,
230
+ path=path,
231
+ content_length=len(content),
232
+ )
233
+
234
+ result = CallToolResult(
235
+ content=[text_content(content)],
236
+ isError=False,
237
+ )
238
+
239
+ # Notify tool handler of completion
240
+ if self._tool_handler and tool_call_id:
241
+ try:
242
+ await self._tool_handler.on_tool_complete(
243
+ tool_call_id, True, result.content, None
244
+ )
245
+ except Exception as e:
246
+ self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
247
+
248
+ return result
249
+
250
+ except Exception as e:
251
+ self.logger.error(
252
+ f"Error reading file: {e}",
253
+ session_id=self.session_id,
254
+ path=path,
255
+ exc_info=True,
256
+ )
257
+
258
+ # Notify tool handler of error
259
+ if self._tool_handler and tool_call_id:
260
+ try:
261
+ await self._tool_handler.on_tool_complete(tool_call_id, False, None, str(e))
262
+ except Exception as handler_error:
263
+ self.logger.error(
264
+ f"Error in tool complete handler: {handler_error}", exc_info=True
265
+ )
266
+
267
+ return CallToolResult(
268
+ content=[text_content(f"Error reading file: {e}")],
269
+ isError=True,
270
+ )
271
+
272
+ async def write_text_file(
273
+ self, arguments: dict[str, Any], tool_use_id: str | None = None
274
+ ) -> CallToolResult:
275
+ """
276
+ Write a text file using ACP filesystem support.
277
+
278
+ Args:
279
+ arguments: Tool arguments containing 'path' and 'content'
280
+ tool_use_id: LLM's tool use ID (for matching with stream events)
281
+
282
+ Returns:
283
+ CallToolResult indicating success or failure
284
+ """
285
+ # Validate arguments
286
+ if not isinstance(arguments, dict):
287
+ return CallToolResult(
288
+ content=[text_content("Error: arguments must be a dict")],
289
+ isError=True,
290
+ )
291
+
292
+ path = arguments.get("path")
293
+ if not path or not isinstance(path, str):
294
+ return CallToolResult(
295
+ content=[text_content("Error: 'path' argument is required and must be a string")],
296
+ isError=True,
297
+ )
298
+
299
+ content = arguments.get("content")
300
+ if content is None or not isinstance(content, str):
301
+ return CallToolResult(
302
+ content=[
303
+ text_content("Error: 'content' argument is required and must be a string")
304
+ ],
305
+ isError=True,
306
+ )
307
+
308
+ self.logger.info(
309
+ "Writing file via ACP filesystem",
310
+ session_id=self.session_id,
311
+ path=path,
312
+ content_length=len(content),
313
+ )
314
+
315
+ # Check permission before execution
316
+ if self._permission_handler:
317
+ try:
318
+ permission_result = await self._permission_handler.check_permission(
319
+ tool_name="write_text_file",
320
+ server_name="acp_filesystem",
321
+ arguments=arguments,
322
+ tool_use_id=tool_use_id,
323
+ )
324
+ if not permission_result.allowed:
325
+ error_msg = permission_result.error_message or (
326
+ f"Permission denied for writing file: {path}"
327
+ )
328
+ self.logger.info(
329
+ "File write denied by permission handler",
330
+ data={
331
+ "path": path,
332
+ "cancelled": permission_result.is_cancelled,
333
+ },
334
+ )
335
+ return CallToolResult(
336
+ content=[text_content(error_msg)],
337
+ isError=True,
338
+ )
339
+ except Exception as e:
340
+ self.logger.error(f"Error checking file write permission: {e}", exc_info=True)
341
+ # Fail-safe: deny on permission check error
342
+ return CallToolResult(
343
+ content=[text_content(f"Permission check failed: {e}")],
344
+ isError=True,
345
+ )
346
+
347
+ # Notify tool handler that execution is starting
348
+ tool_call_id = None
349
+ if self._tool_handler:
350
+ try:
351
+ tool_call_id = await self._tool_handler.on_tool_start(
352
+ "write_text_file", "acp_filesystem", arguments, tool_use_id
353
+ )
354
+ except Exception as e:
355
+ self.logger.error(f"Error in tool start handler: {e}", exc_info=True)
356
+
357
+ try:
358
+ # Send request using the proper ACP method with flattened parameters
359
+ await self.connection.write_text_file(
360
+ content=content,
361
+ path=path,
362
+ session_id=self.session_id,
363
+ )
364
+
365
+ self.logger.info(
366
+ "File write completed",
367
+ session_id=self.session_id,
368
+ path=path,
369
+ )
370
+
371
+ result = CallToolResult(
372
+ content=[text_content(f"Successfully wrote {len(content)} characters to {path}")],
373
+ isError=False,
374
+ )
375
+
376
+ # Notify tool handler of completion
377
+ if self._tool_handler and tool_call_id:
378
+ try:
379
+ await self._tool_handler.on_tool_complete(
380
+ tool_call_id, True, result.content, None
381
+ )
382
+ except Exception as e:
383
+ self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
384
+
385
+ return result
386
+
387
+ except Exception as e:
388
+ self.logger.error(
389
+ f"Error writing file: {e}",
390
+ session_id=self.session_id,
391
+ path=path,
392
+ exc_info=True,
393
+ )
394
+
395
+ # Notify tool handler of error
396
+ if self._tool_handler and tool_call_id:
397
+ try:
398
+ await self._tool_handler.on_tool_complete(tool_call_id, False, None, str(e))
399
+ except Exception as handler_error:
400
+ self.logger.error(
401
+ f"Error in tool complete handler: {handler_error}", exc_info=True
402
+ )
403
+
404
+ return CallToolResult(
405
+ content=[text_content(f"Error writing file: {e}")],
406
+ isError=True,
407
+ )
408
+
409
+ def metadata(self) -> dict[str, Any]:
410
+ """
411
+ Get metadata about this runtime for display/logging.
412
+
413
+ Returns:
414
+ Dict with runtime information
415
+ """
416
+ enabled_tools = []
417
+ if self._enable_read:
418
+ enabled_tools.append("read_text_file")
419
+ if self._enable_write:
420
+ enabled_tools.append("write_text_file")
421
+
422
+ return {
423
+ "type": "acp_filesystem",
424
+ "session_id": self.session_id,
425
+ "activation_reason": self.activation_reason,
426
+ "tools": enabled_tools,
427
+ }
@@ -0,0 +1,269 @@
1
+ """
2
+ ACP Tool Permission Store
3
+
4
+ Provides persistent storage for tool execution permissions.
5
+ Stores permissions in a human-readable markdown file at .fast-agent/auths.md.
6
+ """
7
+
8
+ import asyncio
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from pathlib import Path
12
+
13
+ from fast_agent.core.logging.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ # Default path relative to session working directory
18
+ DEFAULT_PERMISSIONS_FILE = ".fast-agent/auths.md"
19
+
20
+
21
+ class PermissionDecision(str, Enum):
22
+ """Stored permission decisions (only 'always' variants are persisted)."""
23
+
24
+ ALLOW_ALWAYS = "allow_always"
25
+ REJECT_ALWAYS = "reject_always"
26
+
27
+
28
+ @dataclass
29
+ class PermissionResult:
30
+ """Result of a permission check or request."""
31
+
32
+ allowed: bool
33
+ remember: bool = False
34
+ is_cancelled: bool = False
35
+
36
+ @classmethod
37
+ def allow_once(cls) -> "PermissionResult":
38
+ """Create an allow-once result (not persisted)."""
39
+ return cls(allowed=True, remember=False)
40
+
41
+ @classmethod
42
+ def allow_always(cls) -> "PermissionResult":
43
+ """Create an allow-always result (persisted)."""
44
+ return cls(allowed=True, remember=True)
45
+
46
+ @classmethod
47
+ def reject_once(cls) -> "PermissionResult":
48
+ """Create a reject-once result (not persisted)."""
49
+ return cls(allowed=False, remember=False)
50
+
51
+ @classmethod
52
+ def reject_always(cls) -> "PermissionResult":
53
+ """Create a reject-always result (persisted)."""
54
+ return cls(allowed=False, remember=True)
55
+
56
+ @classmethod
57
+ def cancelled(cls) -> "PermissionResult":
58
+ """Create a cancelled result (rejected, not persisted)."""
59
+ return cls(allowed=False, remember=False, is_cancelled=True)
60
+
61
+
62
+ class PermissionStore:
63
+ """
64
+ Persistent storage for tool execution permissions.
65
+
66
+ Stores allow_always and reject_always decisions in a markdown file
67
+ that is human-readable and editable. The file is only created when
68
+ the first 'always' permission is set.
69
+
70
+ Thread-safe for concurrent access using asyncio locks.
71
+ """
72
+
73
+ def __init__(self, cwd: str | Path | None = None) -> None:
74
+ """
75
+ Initialize the permission store.
76
+
77
+ Args:
78
+ cwd: Working directory for the session. If None, uses current directory.
79
+ """
80
+ self._cwd = Path(cwd) if cwd else Path.cwd()
81
+ self._file_path = self._cwd / DEFAULT_PERMISSIONS_FILE
82
+ self._cache: dict[str, PermissionDecision] = {}
83
+ self._loaded = False
84
+ self._lock = asyncio.Lock()
85
+
86
+ @property
87
+ def file_path(self) -> Path:
88
+ """Get the path to the permissions file."""
89
+ return self._file_path
90
+
91
+ def _get_permission_key(self, server_name: str, tool_name: str) -> str:
92
+ """Get a unique key for a server/tool combination."""
93
+ return f"{server_name}/{tool_name}"
94
+
95
+ async def _ensure_loaded(self) -> None:
96
+ """Ensure permissions are loaded from disk (lazy loading)."""
97
+ if self._loaded:
98
+ return
99
+
100
+ if self._file_path.exists():
101
+ try:
102
+ await self._load_from_file()
103
+ except Exception as e:
104
+ logger.warning(
105
+ f"Failed to load permissions file: {e}",
106
+ name="permission_store_load_error",
107
+ )
108
+ # Continue without persisted permissions
109
+ self._loaded = True
110
+
111
+ async def _load_from_file(self) -> None:
112
+ """Load permissions from the markdown file."""
113
+ content = await asyncio.to_thread(self._file_path.read_text, encoding="utf-8")
114
+
115
+ # Parse markdown table format:
116
+ # | Server | Tool | Permission |
117
+ # |--------|------|------------|
118
+ # | server1 | tool1 | allow_always |
119
+
120
+ in_table = False
121
+ for line in content.splitlines():
122
+ line = line.strip()
123
+
124
+ # Skip empty lines and header
125
+ if not line:
126
+ continue
127
+ if line.startswith("# "):
128
+ continue
129
+ if line.startswith("|--") or line.startswith("| --"):
130
+ in_table = True
131
+ continue
132
+ if line.startswith("| Server"):
133
+ continue
134
+
135
+ # Parse table rows
136
+ if in_table and line.startswith("|") and line.endswith("|"):
137
+ parts = [p.strip() for p in line.split("|")[1:-1]]
138
+ if len(parts) >= 3:
139
+ server_name, tool_name, permission = parts[0], parts[1], parts[2]
140
+ key = self._get_permission_key(server_name, tool_name)
141
+ try:
142
+ self._cache[key] = PermissionDecision(permission)
143
+ except ValueError:
144
+ logger.warning(
145
+ f"Invalid permission value in auths.md: {permission}",
146
+ name="permission_store_parse_error",
147
+ )
148
+
149
+ async def _save_to_file(self) -> None:
150
+ """Save permissions to the markdown file."""
151
+ if not self._cache:
152
+ # Don't create file if no permissions to save
153
+ return
154
+
155
+ # Ensure directory exists
156
+ self._file_path.parent.mkdir(parents=True, exist_ok=True)
157
+
158
+ # Build markdown content
159
+ lines = [
160
+ "# fast-agent Tool Permissions",
161
+ "",
162
+ "This file stores persistent tool execution permissions.",
163
+ "You can edit this file manually to add or remove permissions.",
164
+ "",
165
+ "| Server | Tool | Permission |",
166
+ "|--------|------|------------|",
167
+ ]
168
+
169
+ for key, decision in sorted(self._cache.items()):
170
+ server_name, tool_name = key.split("/", 1)
171
+ lines.append(f"| {server_name} | {tool_name} | {decision.value} |")
172
+
173
+ lines.append("") # Trailing newline
174
+ content = "\n".join(lines)
175
+
176
+ await asyncio.to_thread(self._file_path.write_text, content, encoding="utf-8")
177
+
178
+ logger.debug(
179
+ f"Saved {len(self._cache)} permissions to {self._file_path}",
180
+ name="permission_store_saved",
181
+ )
182
+
183
+ async def get(self, server_name: str, tool_name: str) -> PermissionDecision | None:
184
+ """
185
+ Get stored permission for a server/tool.
186
+
187
+ Args:
188
+ server_name: Name of the MCP server
189
+ tool_name: Name of the tool
190
+
191
+ Returns:
192
+ PermissionDecision if stored, None if not found
193
+ """
194
+ async with self._lock:
195
+ await self._ensure_loaded()
196
+ key = self._get_permission_key(server_name, tool_name)
197
+ return self._cache.get(key)
198
+
199
+ async def set(self, server_name: str, tool_name: str, decision: PermissionDecision) -> None:
200
+ """
201
+ Store a permission decision.
202
+
203
+ Args:
204
+ server_name: Name of the MCP server
205
+ tool_name: Name of the tool
206
+ decision: The permission decision to store
207
+ """
208
+ async with self._lock:
209
+ await self._ensure_loaded()
210
+ key = self._get_permission_key(server_name, tool_name)
211
+ self._cache[key] = decision
212
+ try:
213
+ await self._save_to_file()
214
+ except Exception as e:
215
+ logger.warning(
216
+ f"Failed to save permissions file: {e}",
217
+ name="permission_store_save_error",
218
+ )
219
+ # Continue - in-memory cache is still valid
220
+
221
+ async def remove(self, server_name: str, tool_name: str) -> bool:
222
+ """
223
+ Remove a stored permission.
224
+
225
+ Args:
226
+ server_name: Name of the MCP server
227
+ tool_name: Name of the tool
228
+
229
+ Returns:
230
+ True if permission was removed, False if not found
231
+ """
232
+ async with self._lock:
233
+ await self._ensure_loaded()
234
+ key = self._get_permission_key(server_name, tool_name)
235
+ if key in self._cache:
236
+ del self._cache[key]
237
+ try:
238
+ await self._save_to_file()
239
+ except Exception as e:
240
+ logger.warning(
241
+ f"Failed to save permissions file after removal: {e}",
242
+ name="permission_store_save_error",
243
+ )
244
+ return True
245
+ return False
246
+
247
+ async def clear(self) -> None:
248
+ """Clear all stored permissions."""
249
+ async with self._lock:
250
+ self._cache.clear()
251
+ if self._file_path.exists():
252
+ try:
253
+ await asyncio.to_thread(self._file_path.unlink)
254
+ except Exception as e:
255
+ logger.warning(
256
+ f"Failed to delete permissions file: {e}",
257
+ name="permission_store_delete_error",
258
+ )
259
+
260
+ async def list_all(self) -> dict[str, PermissionDecision]:
261
+ """
262
+ Get all stored permissions.
263
+
264
+ Returns:
265
+ Dictionary of permission key -> decision
266
+ """
267
+ async with self._lock:
268
+ await self._ensure_loaded()
269
+ return dict(self._cache)
@@ -0,0 +1,5 @@
1
+ """ACP server implementation."""
2
+
3
+ from fast_agent.acp.server.agent_acp_server import AgentACPServer
4
+
5
+ __all__ = ["AgentACPServer"]