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,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
|