From 019fab5b5f10e3559985b6e682060c1f915235ef Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:25:40 +0800 Subject: [PATCH 01/18] Update tool.py --- astrbot/core/agent/tool.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 4cee6ba6d1..278c220eec 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -58,6 +58,11 @@ class FunctionTool(ToolSchema, Generic[TContext]): Whether the tool is active. This field is a special field for AstrBot. You can ignore it when integrating with other frameworks. """ + require_admin: bool = False + """ + If True, this tool can only be called by admin users (event.role == 'admin'). + Default False keeps backward compatibility with existing tools. + """ is_background_task: bool = False """ Declare this tool as a background task. Background tasks return immediately From 2b38a01c22b8d1dc06bd3b48315cdf022dc819d1 Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:26:31 +0800 Subject: [PATCH 02/18] Update func_tool_manager.py --- astrbot/core/provider/func_tool_manager.py | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index ab6dd037f4..48d237e5f3 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -516,6 +516,8 @@ async def init_mcp_clients( if raise_on_all_failed: raise MCPAllServicesFailedError(msg) logger.error(msg) + + self._restore_tool_permissions() return summary async def _start_mcp_server( @@ -887,6 +889,52 @@ def activate_llm_tool(self, name: str, star_map: dict) -> bool: return True return False + def set_tool_require_admin(self, name: str, require: bool) -> bool: + """设置某工具是否仅管理员可调用,并持久化。 + + Returns: + 如果没找到工具,会返回 False + """ + func_tool = self.get_func(name) + if func_tool is None: + return False + + func_tool.require_admin = require + + require_admin_tools: list = sp.get( + "tool_require_admin_list", + [], + scope="global", + scope_id="global", + ) + if require: + if name not in require_admin_tools: + require_admin_tools.append(name) + else: + if name in require_admin_tools: + require_admin_tools.remove(name) + sp.put( + "tool_require_admin_list", + require_admin_tools, + scope="global", + scope_id="global", + ) + return True + + def _restore_tool_permissions(self) -> None: + """从持久化存储恢复 require_admin 状态到已注册的工具。 + + 应在 MCP 客户端初始化完成后、插件工具注册完成后调用。 + """ + require_admin_tools: list = sp.get( + "tool_require_admin_list", + [], + scope="global", + scope_id="global", + ) + for tool in self.func_list: + tool.require_admin = tool.name in require_admin_tools + @property def mcp_config_path(self): data_dir = get_astrbot_data_path() From 319d3133b58c035f52fa4b662f10d19a7b241b89 Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:27:29 +0800 Subject: [PATCH 03/18] Update star_manager.py --- astrbot/core/star/star_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 824c3b653b..3503d05360 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1269,6 +1269,8 @@ async def load( logger.error(traceback.format_exc()) self._rebuild_failed_plugin_info() + # 插件工具注册完成后恢复权限配置 + llm_tools._restore_tool_permissions() if has_load_error: return False, self.failed_plugin_info return True, None From 9807a6e0c42f8e072b94cfcaae03ca47d6dba98e Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:28:31 +0800 Subject: [PATCH 04/18] Update astr_main_agent.py --- astrbot/core/astr_main_agent.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 1c4fd400a0..dd024ba6b4 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -958,6 +958,29 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: req.func_tool = new_tool_set +def _filter_tools_by_role(event: AstrMessageEvent, req: ProviderRequest) -> None: + """从 req.func_tool 中移除当前用户无权调用的工具。 + + require_admin=True 的工具对非管理员用户不可见, + LLM 不会感知到这些工具的存在。 + """ + if not req.func_tool: + return + if getattr(event, "role", "member") == "admin": + return + new_tool_set = ToolSet() + for tool in req.func_tool.tools: + if getattr(tool, "require_admin", False): + logger.debug( + "Hiding tool '%s' from non-admin user (sender_id=%s).", + tool.name, + event.get_sender_id(), + ) + continue + new_tool_set.add_tool(tool) + req.func_tool = new_tool_set + + async def _handle_webchat( event: AstrMessageEvent, req: ProviderRequest, prov: Provider ) -> None: @@ -1446,6 +1469,7 @@ async def build_main_agent( req.session_id = event.unified_msg_origin _plugin_tool_fix(event, req) + _filter_tools_by_role(event, req) await _apply_web_search_tools(event, req, plugin_context) if config.llm_safety_mode: From 0372ae605c7d2db1a35a2343c78258b04986b2ee Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:29:06 +0800 Subject: [PATCH 05/18] Update astr_agent_tool_exec.py --- astrbot/core/astr_agent_tool_exec.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index de5caad554..b30601d4ab 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -266,11 +266,15 @@ def _build_handoff_toolset( # "all tools", including runtime computer-use tools. if tools is None: toolset = ToolSet() + is_admin = getattr(event, "role", "member") == "admin" for registered_tool in llm_tools.func_list: if isinstance(registered_tool, HandoffTool): continue - if registered_tool.active: - toolset.add_tool(registered_tool) + if not registered_tool.active: + continue + if not is_admin and getattr(registered_tool, "require_admin", False): + continue + toolset.add_tool(registered_tool) for runtime_tool in runtime_computer_tools.values(): toolset.add_tool(runtime_tool) return None if toolset.empty() else toolset @@ -279,10 +283,15 @@ def _build_handoff_toolset( return None toolset = ToolSet() + is_admin = getattr(event, "role", "member") == "admin" for tool_name_or_obj in tools: if isinstance(tool_name_or_obj, str): registered_tool = llm_tools.get_func(tool_name_or_obj) if registered_tool and registered_tool.active: + if not is_admin and getattr( + registered_tool, "require_admin", False + ): + continue toolset.add_tool(registered_tool) continue runtime_tool = runtime_computer_tools.get(tool_name_or_obj) From a59eb4f0aee522d3648b4ec1bad515e5db417e2e Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:29:58 +0800 Subject: [PATCH 06/18] Update tool_loop_agent_runner.py --- .../agent/runners/tool_loop_agent_runner.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 3f74f0ec9b..36621c0a47 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -1043,6 +1043,33 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: ) continue + # 统一权限拦截(过滤点 B:双保险) + # 即使 LLM 意外获知了受限工具,执行前也会在此被阻断。 + if getattr(func_tool, "require_admin", False): + _caller_event = self.run_context.context.event + if getattr(_caller_event, "role", "member") != "admin": + sender_id = ( + _caller_event.get_sender_id() + if _caller_event + else "unknown" + ) + logger.warning( + "Tool '%s' requires admin, " + "caller role='%s', sender_id='%s'. Blocked.", + func_tool_name, + getattr(_caller_event, "role", "?"), + sender_id, + ) + _append_tool_call_result( + func_tool_id, + f"error: Permission denied. " + f"Tool '{func_tool_name}' requires admin privileges. " + f"Your user ID is {sender_id}. " + "Ask the AstrBot admin to grant access via " + "WebUI → 管理行为 → 函数工具 → 权限列.", + ) + continue + valid_params = {} # 参数过滤:只传递函数实际需要的参数 # 获取实际的 handler 函数 From dfa8c0fb421fc40e7b129b619458bb1568c72a4c Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:30:26 +0800 Subject: [PATCH 07/18] Update tools.py --- astrbot/dashboard/routes/tools.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 157b4d75bf..f288d843c7 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -55,6 +55,7 @@ def __init__( "/tools/mcp/test": ("POST", self.test_mcp_connection), "/tools/list": ("GET", self.get_tool_list), "/tools/toggle-tool": ("POST", self.toggle_tool), + "/tools/set-permission": ("POST", self.set_tool_permission), "/tools/mcp/sync-provider": ("POST", self.sync_provider), } self.register_routes() @@ -498,6 +499,7 @@ async def get_tool_list(self): "description": tool.description, "parameters": tool.parameters, "active": tool.active, + "require_admin": getattr(tool, "require_admin", False), "origin": origin, "origin_name": origin_name, "readonly": readonly, @@ -551,6 +553,42 @@ async def toggle_tool(self): logger.error(traceback.format_exc()) return Response().error(f"Failed to operate tool: {e!s}").__dict__ + async def set_tool_permission(self): + """Set require_admin flag for a specified tool.""" + try: + data = await request.json + tool_name = data.get("name") + require_admin = data.get("require_admin") + + if not tool_name or require_admin is None: + return ( + Response() + .error("Missing required parameters: name or require_admin") + .__dict__ + ) + + if self.tool_mgr.is_builtin_tool(tool_name): + return ( + Response() + .error( + f"Tool {tool_name} is a builtin tool and its permission cannot be changed." + ) + .__dict__ + ) + + ok = self.tool_mgr.set_tool_require_admin(tool_name, bool(require_admin)) + if ok: + return Response().ok(None, "Permission updated.").__dict__ + return ( + Response() + .error(f"Tool {tool_name} does not exist or the operation failed.") + .__dict__ + ) + + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"Failed to set tool permission: {e!s}").__dict__ + async def sync_provider(self): """Sync MCP provider configuration.""" try: From 98c0b948754577d8d8131d8826f585adfadf55f4 Mon Sep 17 00:00:00 2001 From: lingyun14 Date: Sat, 6 Jun 2026 12:31:30 +0800 Subject: [PATCH 08/18] Update ToolTable.vue --- .../componentPanel/components/ToolTable.vue | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/dashboard/src/components/extension/componentPanel/components/ToolTable.vue b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue index 8a42f33fef..83bf8de54c 100644 --- a/dashboard/src/components/extension/componentPanel/components/ToolTable.vue +++ b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue @@ -12,6 +12,7 @@ const props = defineProps<{ const emit = defineEmits<{ (e: 'toggle-tool', tool: ToolItem): void; + (e: 'update-permission', tool: ToolItem, permission: 'admin' | 'everyone'): void; }>(); const toolHeaders = computed(() => [ @@ -19,6 +20,7 @@ const toolHeaders = computed(() => [ { title: tmTool('functionTools.description'), key: 'description' }, { title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' }, { title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' }, + { title: tmTool('functionTools.table.permission'), key: 'permission', sortable: false, width: '120px' }, { title: tmTool('functionTools.table.actions'), key: 'actions', sortable: false, width: '120px' } ]); @@ -69,6 +71,16 @@ const enabledConfigTags = (tool: ToolItem): BuiltinToolConfigTag[] => { if (tool.origin !== 'builtin') return []; return (tool.builtin_config_tags || []).filter(tag => tag.enabled); }; + +const getPermissionColor = (permission: string): string => { + return permission === 'admin' ? 'error' : 'success'; +}; + +const getPermissionLabel = (permission: string): string => { + return permission === 'admin' + ? tmTool('functionTools.permission.admin') + : tmTool('functionTools.permission.everyone'); +}; + + +