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