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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACPTerminalRuntime - Execute commands via ACP terminal support.
|
|
3
|
+
|
|
4
|
+
This runtime allows FastAgent to execute commands through the ACP client's terminal
|
|
5
|
+
capabilities when available (e.g., in Zed editor). This provides better integration
|
|
6
|
+
compared to local process execution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from mcp.types import CallToolResult, Tool
|
|
13
|
+
|
|
14
|
+
from fast_agent.constants import DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT
|
|
15
|
+
from fast_agent.core.logging.logger import get_logger
|
|
16
|
+
from fast_agent.mcp.helpers.content_helpers import text_content
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from acp import AgentSideConnection
|
|
20
|
+
|
|
21
|
+
from fast_agent.mcp.tool_execution_handler import ToolExecutionHandler
|
|
22
|
+
from fast_agent.mcp.tool_permission_handler import ToolPermissionHandler
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ACPTerminalRuntime:
|
|
28
|
+
"""
|
|
29
|
+
Provides command execution through ACP terminal support.
|
|
30
|
+
|
|
31
|
+
This runtime implements the "execute" tool by delegating to the ACP client's
|
|
32
|
+
terminal capabilities. The flow is:
|
|
33
|
+
1. terminal/create - Start command execution
|
|
34
|
+
2. terminal/wait_for_exit - Wait for completion
|
|
35
|
+
3. terminal/output - Retrieve output
|
|
36
|
+
4. terminal/release - Clean up resources
|
|
37
|
+
|
|
38
|
+
The client (e.g., Zed editor) handles displaying the terminal UI to the user.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
connection: "AgentSideConnection",
|
|
44
|
+
session_id: str,
|
|
45
|
+
activation_reason: str,
|
|
46
|
+
logger_instance=None,
|
|
47
|
+
timeout_seconds: int = 90,
|
|
48
|
+
tool_handler: "ToolExecutionHandler | None" = None,
|
|
49
|
+
default_output_byte_limit: int = DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT,
|
|
50
|
+
permission_handler: "ToolPermissionHandler | None" = None,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the ACP terminal runtime.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
connection: The ACP connection to use for terminal operations
|
|
57
|
+
session_id: The ACP session ID for this runtime
|
|
58
|
+
activation_reason: Human-readable reason for activation
|
|
59
|
+
logger_instance: Optional logger instance
|
|
60
|
+
timeout_seconds: Default timeout for command execution
|
|
61
|
+
tool_handler: Optional tool execution handler for telemetry
|
|
62
|
+
permission_handler: Optional permission handler for tool execution authorization
|
|
63
|
+
"""
|
|
64
|
+
self.connection = connection
|
|
65
|
+
self.session_id = session_id
|
|
66
|
+
self.activation_reason = activation_reason
|
|
67
|
+
self.logger = logger_instance or logger
|
|
68
|
+
self.timeout_seconds = timeout_seconds
|
|
69
|
+
self._tool_handler = tool_handler
|
|
70
|
+
self._default_output_byte_limit = (
|
|
71
|
+
default_output_byte_limit or DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT
|
|
72
|
+
)
|
|
73
|
+
self._permission_handler = permission_handler
|
|
74
|
+
|
|
75
|
+
# Tool definition for LLM
|
|
76
|
+
self._tool = Tool(
|
|
77
|
+
name="execute",
|
|
78
|
+
description="Execute a shell command.",
|
|
79
|
+
inputSchema={
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"command": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "The shell command to execute. Do not include shell "
|
|
85
|
+
"prefix (bash -c, etc.).",
|
|
86
|
+
},
|
|
87
|
+
"args": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {"type": "string"},
|
|
90
|
+
"description": "Optional array of command arguments (alternative to including in command string).",
|
|
91
|
+
},
|
|
92
|
+
"env": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"description": "Optional environment variables as key-value pairs.",
|
|
95
|
+
"additionalProperties": {"type": "string"},
|
|
96
|
+
},
|
|
97
|
+
"cwd": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "Optional absolute path for working directory.",
|
|
100
|
+
},
|
|
101
|
+
# Do not allow model to handle this for the moment.
|
|
102
|
+
# "outputByteLimit": {
|
|
103
|
+
# "type": "integer",
|
|
104
|
+
# "description": "Maximum bytes of output to retain. (prevents unbounded buffers).",
|
|
105
|
+
# },
|
|
106
|
+
},
|
|
107
|
+
"required": ["command"],
|
|
108
|
+
"additionalProperties": False,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.logger.info(
|
|
113
|
+
"ACPTerminalRuntime initialized",
|
|
114
|
+
session_id=session_id,
|
|
115
|
+
reason=activation_reason,
|
|
116
|
+
timeout=timeout_seconds,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def tool(self) -> Tool:
|
|
121
|
+
"""Get the execute tool definition."""
|
|
122
|
+
return self._tool
|
|
123
|
+
|
|
124
|
+
async def execute(
|
|
125
|
+
self, arguments: dict[str, Any], tool_use_id: str | None = None
|
|
126
|
+
) -> CallToolResult:
|
|
127
|
+
"""
|
|
128
|
+
Execute a command using ACP terminal support.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
arguments: Tool arguments containing 'command' key
|
|
132
|
+
tool_use_id: LLM's tool use ID (for matching with stream events)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
CallToolResult with command output and exit status
|
|
136
|
+
"""
|
|
137
|
+
# Validate arguments
|
|
138
|
+
if not isinstance(arguments, dict):
|
|
139
|
+
return CallToolResult(
|
|
140
|
+
content=[text_content("Error: arguments must be a dict")],
|
|
141
|
+
isError=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
command = arguments.get("command")
|
|
145
|
+
if not command or not isinstance(command, str):
|
|
146
|
+
return CallToolResult(
|
|
147
|
+
content=[
|
|
148
|
+
text_content("Error: 'command' argument is required and must be a string")
|
|
149
|
+
],
|
|
150
|
+
isError=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.logger.info(
|
|
154
|
+
"Executing command via ACP terminal",
|
|
155
|
+
session_id=self.session_id,
|
|
156
|
+
command=command[:100], # Log first 100 chars
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Check permission before execution
|
|
160
|
+
if self._permission_handler:
|
|
161
|
+
try:
|
|
162
|
+
permission_result = await self._permission_handler.check_permission(
|
|
163
|
+
tool_name="execute",
|
|
164
|
+
server_name="acp_terminal",
|
|
165
|
+
arguments=arguments,
|
|
166
|
+
tool_use_id=tool_use_id,
|
|
167
|
+
)
|
|
168
|
+
if not permission_result.allowed:
|
|
169
|
+
error_msg = permission_result.error_message or (
|
|
170
|
+
"Permission denied for terminal execution"
|
|
171
|
+
)
|
|
172
|
+
self.logger.info(
|
|
173
|
+
"Terminal execution denied by permission handler",
|
|
174
|
+
data={
|
|
175
|
+
"command": command[:100],
|
|
176
|
+
"cancelled": permission_result.is_cancelled,
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
return CallToolResult(
|
|
180
|
+
content=[text_content(error_msg)],
|
|
181
|
+
isError=True,
|
|
182
|
+
)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self.logger.error(f"Error checking terminal permission: {e}", exc_info=True)
|
|
185
|
+
# Fail-safe: deny on permission check error
|
|
186
|
+
return CallToolResult(
|
|
187
|
+
content=[text_content(f"Permission check failed: {e}")],
|
|
188
|
+
isError=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Notify tool handler that execution is starting
|
|
192
|
+
tool_call_id = None
|
|
193
|
+
if self._tool_handler:
|
|
194
|
+
try:
|
|
195
|
+
tool_call_id = await self._tool_handler.on_tool_start(
|
|
196
|
+
"execute", "acp_terminal", arguments, tool_use_id
|
|
197
|
+
)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self.logger.error(f"Error in tool start handler: {e}", exc_info=True)
|
|
200
|
+
|
|
201
|
+
terminal_id = None # Will be set by client in terminal/create response
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Step 1: Create terminal and start command execution
|
|
205
|
+
# NOTE: Client creates and returns the terminal ID, we don't generate it
|
|
206
|
+
self.logger.debug("Creating terminal")
|
|
207
|
+
|
|
208
|
+
# Build create params per ACP spec (sessionId, command, args, env, cwd, outputByteLimit)
|
|
209
|
+
# Extract optional parameters from arguments
|
|
210
|
+
create_params: dict[str, Any] = {
|
|
211
|
+
"sessionId": self.session_id,
|
|
212
|
+
"command": command,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Add optional parameters if provided
|
|
216
|
+
if args := arguments.get("args"):
|
|
217
|
+
create_params["args"] = args
|
|
218
|
+
if env := arguments.get("env"):
|
|
219
|
+
# Transform env from object format (LLM-friendly) to ACP array format
|
|
220
|
+
# Input: {"PATH": "/usr/bin", "HOME": "/home/user"}
|
|
221
|
+
# Output: [{"name": "PATH", "value": "/usr/bin"}, {"name": "HOME", "value": "/home/user"}]
|
|
222
|
+
if isinstance(env, dict):
|
|
223
|
+
create_params["env"] = [
|
|
224
|
+
{"name": name, "value": value} for name, value in env.items()
|
|
225
|
+
]
|
|
226
|
+
else:
|
|
227
|
+
# If already in array format, pass through
|
|
228
|
+
create_params["env"] = env
|
|
229
|
+
if cwd := arguments.get("cwd"):
|
|
230
|
+
create_params["cwd"] = cwd
|
|
231
|
+
if "outputByteLimit" in arguments and arguments["outputByteLimit"] is not None:
|
|
232
|
+
create_params["outputByteLimit"] = arguments["outputByteLimit"]
|
|
233
|
+
else:
|
|
234
|
+
create_params["outputByteLimit"] = self._default_output_byte_limit
|
|
235
|
+
|
|
236
|
+
create_result = await self.connection._conn.send_request(
|
|
237
|
+
"terminal/create", create_params
|
|
238
|
+
)
|
|
239
|
+
terminal_id = create_result.get("terminalId")
|
|
240
|
+
|
|
241
|
+
if not terminal_id:
|
|
242
|
+
return CallToolResult(
|
|
243
|
+
content=[text_content("Error: Client did not return terminal ID")],
|
|
244
|
+
isError=True,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self.logger.debug(f"Terminal created with ID: {terminal_id}")
|
|
248
|
+
|
|
249
|
+
# Step 2: Wait for command to complete (with timeout)
|
|
250
|
+
self.logger.debug(f"Waiting for terminal {terminal_id} to exit")
|
|
251
|
+
try:
|
|
252
|
+
wait_params = {"sessionId": self.session_id, "terminalId": terminal_id}
|
|
253
|
+
wait_result = await asyncio.wait_for(
|
|
254
|
+
self.connection._conn.send_request("terminal/wait_for_exit", wait_params),
|
|
255
|
+
timeout=self.timeout_seconds,
|
|
256
|
+
)
|
|
257
|
+
exit_code = wait_result.get("exitCode", -1)
|
|
258
|
+
signal = wait_result.get("signal")
|
|
259
|
+
except asyncio.TimeoutError:
|
|
260
|
+
self.logger.warning(
|
|
261
|
+
f"Terminal {terminal_id} timed out after {self.timeout_seconds}s"
|
|
262
|
+
)
|
|
263
|
+
# Kill the terminal
|
|
264
|
+
try:
|
|
265
|
+
kill_params = {"sessionId": self.session_id, "terminalId": terminal_id}
|
|
266
|
+
await self.connection._conn.send_request("terminal/kill", kill_params)
|
|
267
|
+
except Exception as kill_error:
|
|
268
|
+
self.logger.error(f"Error killing terminal: {kill_error}")
|
|
269
|
+
|
|
270
|
+
# Still try to get output
|
|
271
|
+
output_params = {"sessionId": self.session_id, "terminalId": terminal_id}
|
|
272
|
+
output_result = await self.connection._conn.send_request(
|
|
273
|
+
"terminal/output", output_params
|
|
274
|
+
)
|
|
275
|
+
output_text = output_result.get("output", "")
|
|
276
|
+
|
|
277
|
+
# Release terminal
|
|
278
|
+
await self._release_terminal(terminal_id)
|
|
279
|
+
|
|
280
|
+
timeout_result = CallToolResult(
|
|
281
|
+
content=[
|
|
282
|
+
text_content(
|
|
283
|
+
f"Command timed out after {self.timeout_seconds}s\n\n"
|
|
284
|
+
f"Output so far:\n{output_text}"
|
|
285
|
+
)
|
|
286
|
+
],
|
|
287
|
+
isError=True,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Notify tool handler of timeout error
|
|
291
|
+
if self._tool_handler and tool_call_id:
|
|
292
|
+
try:
|
|
293
|
+
await self._tool_handler.on_tool_complete(
|
|
294
|
+
tool_call_id,
|
|
295
|
+
False,
|
|
296
|
+
None,
|
|
297
|
+
f"Command timed out after {self.timeout_seconds}s",
|
|
298
|
+
)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
|
|
301
|
+
|
|
302
|
+
return timeout_result
|
|
303
|
+
|
|
304
|
+
# Step 3: Get the output
|
|
305
|
+
self.logger.debug(f"Retrieving output from terminal {terminal_id}")
|
|
306
|
+
output_params = {"sessionId": self.session_id, "terminalId": terminal_id}
|
|
307
|
+
output_result = await self.connection._conn.send_request(
|
|
308
|
+
"terminal/output", output_params
|
|
309
|
+
)
|
|
310
|
+
output_text = output_result.get("output", "")
|
|
311
|
+
truncated = output_result.get("truncated", False)
|
|
312
|
+
|
|
313
|
+
# Step 4: Release the terminal
|
|
314
|
+
await self._release_terminal(terminal_id)
|
|
315
|
+
|
|
316
|
+
# Format result
|
|
317
|
+
is_error = exit_code != 0
|
|
318
|
+
result_text = output_text
|
|
319
|
+
|
|
320
|
+
if truncated:
|
|
321
|
+
result_text = f"[Output truncated]\n{result_text}"
|
|
322
|
+
|
|
323
|
+
if signal:
|
|
324
|
+
result_text = f"{result_text}\n\n[Terminated by signal: {signal}]"
|
|
325
|
+
|
|
326
|
+
result_text = f"{result_text}\n\n[Exit code: {exit_code}]"
|
|
327
|
+
|
|
328
|
+
self.logger.info(
|
|
329
|
+
"Terminal execution completed",
|
|
330
|
+
terminal_id=terminal_id,
|
|
331
|
+
exit_code=exit_code,
|
|
332
|
+
output_length=len(output_text),
|
|
333
|
+
truncated=truncated,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
result = CallToolResult(
|
|
337
|
+
content=[text_content(result_text)],
|
|
338
|
+
isError=is_error,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Notify tool handler of completion
|
|
342
|
+
if self._tool_handler and tool_call_id:
|
|
343
|
+
try:
|
|
344
|
+
await self._tool_handler.on_tool_complete(
|
|
345
|
+
tool_call_id,
|
|
346
|
+
not is_error,
|
|
347
|
+
result.content if not is_error else None,
|
|
348
|
+
result_text if is_error else None,
|
|
349
|
+
)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
self.logger.error(
|
|
357
|
+
f"Error executing terminal command: {e}",
|
|
358
|
+
terminal_id=terminal_id,
|
|
359
|
+
exc_info=True,
|
|
360
|
+
)
|
|
361
|
+
# Try to clean up if we have a terminal ID
|
|
362
|
+
if terminal_id:
|
|
363
|
+
try:
|
|
364
|
+
await self._release_terminal(terminal_id)
|
|
365
|
+
except Exception:
|
|
366
|
+
pass # Best effort cleanup
|
|
367
|
+
|
|
368
|
+
# Notify tool handler of error
|
|
369
|
+
if self._tool_handler and tool_call_id:
|
|
370
|
+
try:
|
|
371
|
+
await self._tool_handler.on_tool_complete(tool_call_id, False, None, str(e))
|
|
372
|
+
except Exception as handler_error:
|
|
373
|
+
self.logger.error(
|
|
374
|
+
f"Error in tool complete handler: {handler_error}", exc_info=True
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
return CallToolResult(
|
|
378
|
+
content=[text_content(f"Terminal execution error: {e}")],
|
|
379
|
+
isError=True,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
async def _release_terminal(self, terminal_id: str) -> None:
|
|
383
|
+
"""
|
|
384
|
+
Release a terminal (cleanup).
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
terminal_id: The terminal ID to release
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
self.logger.debug(f"Releasing terminal {terminal_id}")
|
|
391
|
+
release_params = {"sessionId": self.session_id, "terminalId": terminal_id}
|
|
392
|
+
await self.connection._conn.send_request("terminal/release", release_params)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
self.logger.error(f"Error releasing terminal {terminal_id}: {e}")
|
|
395
|
+
|
|
396
|
+
def metadata(self) -> dict[str, Any]:
|
|
397
|
+
"""
|
|
398
|
+
Get metadata about this runtime for display/logging.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dict with runtime information
|
|
402
|
+
"""
|
|
403
|
+
return {
|
|
404
|
+
"type": "acp_terminal",
|
|
405
|
+
"session_id": self.session_id,
|
|
406
|
+
"activation_reason": self.activation_reason,
|
|
407
|
+
"timeout_seconds": self.timeout_seconds,
|
|
408
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACP Tool Permission Adapter
|
|
3
|
+
|
|
4
|
+
Bridges ACPToolPermissionManager to the MCP ToolPermissionHandler protocol,
|
|
5
|
+
allowing ACP permission checking to be injected into the MCP aggregator.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from fast_agent.acp.permission_store import PermissionStore
|
|
12
|
+
from fast_agent.acp.tool_permissions import ACPToolPermissionManager
|
|
13
|
+
from fast_agent.mcp.common import create_namespaced_name
|
|
14
|
+
from fast_agent.mcp.tool_permission_handler import ToolPermissionHandler, ToolPermissionResult
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from acp import AgentSideConnection
|
|
18
|
+
|
|
19
|
+
from fast_agent.acp.tool_progress import ACPToolProgressManager
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ACPToolPermissionAdapter(ToolPermissionHandler):
|
|
23
|
+
"""
|
|
24
|
+
Adapts ACPToolPermissionManager to implement the ToolPermissionHandler protocol.
|
|
25
|
+
|
|
26
|
+
This adapter translates between the ACP-specific permission types and the
|
|
27
|
+
generic MCP permission handler interface.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
connection: "AgentSideConnection",
|
|
33
|
+
session_id: str,
|
|
34
|
+
store: PermissionStore | None = None,
|
|
35
|
+
cwd: str | Path | None = None,
|
|
36
|
+
tool_handler: "ACPToolProgressManager | None" = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initialize the adapter.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
connection: The ACP connection to send permission requests on
|
|
43
|
+
session_id: The ACP session ID
|
|
44
|
+
store: Optional PermissionStore for persistence
|
|
45
|
+
cwd: Working directory for the store (only used if store not provided)
|
|
46
|
+
tool_handler: Optional tool progress manager for toolCallId lookup
|
|
47
|
+
"""
|
|
48
|
+
self._tool_handler = tool_handler
|
|
49
|
+
self._manager = ACPToolPermissionManager(
|
|
50
|
+
connection=connection,
|
|
51
|
+
session_id=session_id,
|
|
52
|
+
store=store,
|
|
53
|
+
cwd=cwd,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def manager(self) -> ACPToolPermissionManager:
|
|
58
|
+
"""Access the underlying permission manager."""
|
|
59
|
+
return self._manager
|
|
60
|
+
|
|
61
|
+
async def check_permission(
|
|
62
|
+
self,
|
|
63
|
+
tool_name: str,
|
|
64
|
+
server_name: str,
|
|
65
|
+
arguments: dict[str, Any] | None = None,
|
|
66
|
+
tool_use_id: str | None = None,
|
|
67
|
+
) -> ToolPermissionResult:
|
|
68
|
+
"""
|
|
69
|
+
Check if tool execution is permitted.
|
|
70
|
+
|
|
71
|
+
Delegates to ACPToolPermissionManager and converts the result
|
|
72
|
+
to ToolPermissionResult.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
tool_name: Name of the tool to execute
|
|
76
|
+
server_name: Name of the MCP server providing the tool
|
|
77
|
+
arguments: Tool arguments
|
|
78
|
+
tool_use_id: LLM's tool use ID
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
ToolPermissionResult indicating whether execution is allowed
|
|
82
|
+
"""
|
|
83
|
+
# Look up the ACP toolCallId if a streaming notification was already sent
|
|
84
|
+
# This ensures the permission request references the same tool call
|
|
85
|
+
tool_call_id = tool_use_id
|
|
86
|
+
if tool_use_id and self._tool_handler:
|
|
87
|
+
acp_tool_call_id = await self._tool_handler.get_tool_call_id_for_tool_use(tool_use_id)
|
|
88
|
+
if acp_tool_call_id:
|
|
89
|
+
tool_call_id = acp_tool_call_id
|
|
90
|
+
|
|
91
|
+
result = await self._manager.check_permission(
|
|
92
|
+
tool_name=tool_name,
|
|
93
|
+
server_name=server_name,
|
|
94
|
+
arguments=arguments,
|
|
95
|
+
tool_call_id=tool_call_id,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
namespaced_tool_name = create_namespaced_name(server_name, tool_name)
|
|
99
|
+
|
|
100
|
+
# Convert PermissionResult to ToolPermissionResult
|
|
101
|
+
if result.is_cancelled:
|
|
102
|
+
return ToolPermissionResult.cancelled()
|
|
103
|
+
elif result.allowed:
|
|
104
|
+
return ToolPermissionResult(allowed=True, remember=result.remember)
|
|
105
|
+
else:
|
|
106
|
+
# Distinguish between one-time and persistent rejection for clearer UX
|
|
107
|
+
if result.remember:
|
|
108
|
+
error_message = (
|
|
109
|
+
f"The user has permanently declined permission to use this tool: "
|
|
110
|
+
f"{namespaced_tool_name}"
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
error_message = (
|
|
114
|
+
f"The user has declined permission to use this tool: {namespaced_tool_name}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return ToolPermissionResult(
|
|
118
|
+
allowed=False,
|
|
119
|
+
remember=result.remember,
|
|
120
|
+
error_message=error_message,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def clear_session_cache(self) -> None:
|
|
124
|
+
"""Clear the session-level permission cache."""
|
|
125
|
+
await self._manager.clear_session_cache()
|