Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions astrbot/core/agent/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ class FunctionTool(ToolSchema, Generic[TContext]):
Declare this tool as a background task. Background tasks return immediately
with a task identifier while the real work continues asynchronously.
"""
declared_permission_type: str | None = None
"""
The permission level declared by the tool author via ``@llm_tool(permission_type=...)``.
One of ``"admin"`` / ``"member"`` / ``None``.
This is only a *default*: it is used when the dashboard has no explicit
per-tool permission override configured for this tool. It lets plugin
authors ship a sane default permission requirement (e.g. ADMIN for a
dangerous tool) without requiring the bot owner to visit the WebUI panel.
An explicit override saved via the dashboard always takes precedence.
This field is a special field for AstrBot; you can ignore it when
integrating with other frameworks.
"""

def __repr__(self) -> str:
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
Expand Down
39 changes: 37 additions & 2 deletions astrbot/core/provider/func_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ def spec_to_func(
func_args: list[dict],
desc: str,
handler: Callable[..., Awaitable[Any] | AsyncGenerator[Any]],
declared_permission_type: str | None = None,
) -> FuncTool:
params = {
"type": "object", # hard-coded here
Expand All @@ -358,6 +359,7 @@ def spec_to_func(
parameters=params,
description=desc,
handler=handler,
declared_permission_type=declared_permission_type,
)

def add_func(
Expand All @@ -366,13 +368,17 @@ def add_func(
func_args: list,
desc: str,
handler: Callable[..., Awaitable[Any] | AsyncGenerator[Any]],
declared_permission_type: str | None = None,
) -> None:
"""添加函数调用工具

@param name: 函数名
@param func_args: 函数参数列表,格式为 [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...]
@param desc: 函数描述
@param func_obj: 处理函数
@param declared_permission_type: 插件作者通过 ``@llm_tool(permission_type=...)``
声明的默认权限(``"admin"`` / ``"member"`` / ``None``)。仅在该工具尚未
在 WebUI 面板中被显式配置过权限时生效,作为默认值使用。
"""
# check if the tool has been added before
self.remove_func(name)
Expand All @@ -383,6 +389,7 @@ def add_func(
func_args=func_args,
desc=desc,
handler=handler,
declared_permission_type=declared_permission_type,
),
)
logger.info(f"Added llm tool: {name}")
Expand Down Expand Up @@ -451,10 +458,38 @@ def is_builtin_tool(self, name: str) -> bool:
def _default_permission(self, tool_name: str) -> str:
"""Compute the fallback permission for a non-builtin tool.

All non-builtin tools default to ``"member"`` (no restriction).
Builtin tools are never routed through this method."""
If the tool's author declared a default via
``@llm_tool(permission_type=...)``, that value is used. Otherwise
non-builtin tools default to ``"member"`` (no restriction).
Builtin tools are never routed through this method.

Uses ``getattr`` rather than direct attribute access: third-party
tools registered via ``add_llm_tools()`` are only type-hinted as
``FunctionTool`` but not enforced at runtime, so an older or
custom tool object that doesn't inherit ``FunctionTool`` may not
carry this attribute at all."""
tool = self._find_declared_tool(tool_name)
declared = getattr(tool, "declared_permission_type", None)
Comment thread
lingyun14beta marked this conversation as resolved.
Outdated
if declared in ("admin", "member"):
return declared
return "member"

def _find_declared_tool(self, tool_name: str) -> FuncTool | None:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
"""Find a tool in ``func_list`` by name, preferring the active copy.

Mirrors the lookup precedence of :meth:`get_func` but never falls
through to builtin tools, since builtin tools don't carry a
declared permission and shouldn't trigger builtin-tool loading
here."""
fallback = None
for f in reversed(self.func_list):
if f.name == tool_name:
if getattr(f, "active", True):
return f
if fallback is None:
fallback = f
return fallback

def _check_tool_permission(
self,
tool_name: str,
Expand Down
48 changes: 46 additions & 2 deletions astrbot/core/star/register/star_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,11 @@ def decorator(awaitable):
return decorator


def register_llm_tool(name: str | None = None, **kwargs):
def register_llm_tool(
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
name: str | None = None,
permission_type: PermissionType | None = None,
**kwargs,
):
Comment thread
lingyun14beta marked this conversation as resolved.
"""为函数调用(function-calling / tools-use)添加工具。

请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会尝试解析该函数注释)
Expand Down Expand Up @@ -609,12 +613,46 @@ async def get_weather(event: AstrMessageEvent, location: str):
yield
```

权限声明 / Permission declaration:
可以通过 ``permission_type`` 参数为该工具声明一个默认权限要求,效果类似于
``@filter.command()`` 配合 ``@filter.permission_type()`` 的用法。这样即便
机器人主人从未在 WebUI 面板里手动配置过该工具的权限,工具也会默认拥有这层
防护:

```
@llm_tool(name="restart_server", permission_type=filter.PermissionType.ADMIN)
async def restart_server(event: AstrMessageEvent):
\'\'\'重启服务器。\'\'\'
# 处理逻辑
```

- 该参数只是一个**默认值**:如果机器人主人之后在 WebUI 面板里手动为该工具
设置了权限,面板的设置会覆盖这里声明的默认值。
- 不传或传 ``None`` 时行为与之前完全一致(默认所有人可用)。
- 通过 ``registering_agent``(即 ``Agent.llm_tool``)注册的工具目前不支持
权限声明,因为它们不经过面板可配置的工具管理系统。

"""
name_ = name
registering_agent = None
if kwargs.get("registering_agent"):
registering_agent = kwargs["registering_agent"]

if permission_type is not None and not isinstance(permission_type, PermissionType):
raise ValueError(
"permission_type 必须为 astrbot.api.event.filter.PermissionType 的成员(ADMIN / MEMBER)。",
)
Comment thread
lingyun14beta marked this conversation as resolved.
Outdated
Comment thread
lingyun14beta marked this conversation as resolved.
Outdated
if permission_type is None:
declared_permission = None
elif permission_type == PermissionType.ADMIN:
declared_permission = "admin"
else:
# PermissionType.MEMBER (or any non-ADMIN flag) means "no restriction",
# which is already the implicit default, but we still record it
# explicitly so a dashboard can distinguish "declared member" from
# "never declared anything".
declared_permission = "member"

def decorator(
awaitable: Callable[
...,
Expand Down Expand Up @@ -661,7 +699,13 @@ def decorator(
if not registering_agent:
doc_desc = docstring.description.strip() if docstring.description else ""
md = get_handler_or_create(awaitable, EventType.OnCallingFuncToolEvent)
llm_tools.add_func(llm_tool_name, args, doc_desc, md.handler)
llm_tools.add_func(
llm_tool_name,
args,
doc_desc,
md.handler,
declared_permission_type=declared_permission,
)
else:
assert isinstance(registering_agent, RegisteringAgent)
# print(f"Registering tool {llm_tool_name} for agent", registering_agent._agent.name)
Expand Down
9 changes: 8 additions & 1 deletion astrbot/dashboard/services/tools_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,14 @@ def _serialize_tool(self, tool, config_entries: list[dict]) -> dict:
perms_store.get("_default", {}) if isinstance(perms_store, dict) else {}
)
configured = tool.name in defaults
permission = defaults[tool.name] if configured else "member"
permission = (
defaults[tool.name]
if configured
else self.tool_mgr._default_permission(tool.name)
)
tool_info["permission"] = permission
tool_info["permission_configured"] = configured
tool_info["declared_permission_type"] = getattr(
tool, "declared_permission_type", None
)
return tool_info
7 changes: 7 additions & 0 deletions dashboard/src/components/extension/componentPanel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,11 @@ export interface ToolItem {
permission?: 'admin' | 'member';
/** True when permission was explicitly configured rather than a fallback default. */
permission_configured?: boolean;
/**
* Default permission declared by the plugin author via
* `@llm_tool(permission_type=...)`. `null` when the tool declared no
* default. Distinct from `permission_configured`, which reflects an
* explicit WebUI override that always takes precedence over this value.
*/
declared_permission_type?: 'admin' | 'member' | null;
}
45 changes: 45 additions & 0 deletions docs/en/dev/star/guides/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,51 @@ Supported types: `string`, `number`, `object`, `boolean`, `array`. Since v4.5.7,
>
> Additionally, passing `parameters=...` directly to the decorator is **not supported** and will be silently ignored. If you need manual control over the schema, use the `@dataclass` + `add_llm_tools()` approach above.

### Declaring a Default Permission for Tools

> [!TIP]
> Added in v4.X.X

Just as `@filter.command()` can be restricted to admins with `@filter.permission_type(filter.PermissionType.ADMIN)`, `@filter.llm_tool()` supports the same idea through a `permission_type` parameter, letting you declare a default permission for the tool:

```py
from astrbot.api.event import filter, AstrMessageEvent

@filter.llm_tool(name="restart_server", permission_type=filter.PermissionType.ADMIN)
async def restart_server(self, event: AstrMessageEvent):
'''Restart the server.'''
# handler logic
```

`permission_type` accepts `filter.PermissionType.ADMIN` or `filter.PermissionType.MEMBER`. If omitted, the previous behavior is unchanged (the tool is available to everyone).

If you define a tool via `@dataclass` + `FunctionTool` (see [Defining Tools](#defining-tools) above), you can declare a default permission the same way by adding a `declared_permission_type` field to the dataclass:

```py
from pydantic import Field
from pydantic.dataclasses import dataclass

from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext


@dataclass
class RestartServerTool(FunctionTool[AstrAgentContext]):
name: str = "restart_server"
description: str = "Restart the server."
parameters: dict = Field(default_factory=lambda: {"type": "object", "properties": {}})
declared_permission_type: str | None = "admin" # "admin" / "member" / None

async def call(self, context: ContextWrapper[AstrAgentContext], **kwargs) -> ToolExecResult:
# handler logic
return "ok"
```

> [!WARNING]
> - `permission_type` / `declared_permission_type` only sets the tool's **default permission**. If the bot owner has explicitly configured a permission for this tool in the WebUI panel (Extensions -> Components -> Tool Management), that configuration **overrides** the default declared in the plugin's code.
> - The point of this mechanism is that plugin authors can ship a sane default safeguard for dangerous tools (e.g. restarting a service, running shell commands) without relying on the bot owner to ever open the WebUI panel.

## Invoking Agents

> [!TIP]
Expand Down
45 changes: 45 additions & 0 deletions docs/zh/dev/star/guides/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,51 @@ async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEv
>
> 此外,装饰器**不支持**通过 `parameters=...` 显式传入参数 schema,该写法会被忽略。如需手动控制 schema,请使用上方的 `@dataclass` + `add_llm_tools()` 方式。

### 为 Tool 声明默认权限

> [!TIP]
> 在 v4.X.X 时加入

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Consider a more idiomatic phrasing for the version note.

The wording "在 v4.X.X 时加入" is slightly awkward for written technical docs. Prefer a more standard phrasing such as "在 v4.X.X 中加入" or "在 v4.X.X 中添加".

Suggested change
> 在 v4.X.X 时加入
> 在 v4.X.X 中加入


`@filter.command()` 可以通过 `@filter.permission_type(filter.PermissionType.ADMIN)` 限制指令仅管理员可用,`@filter.llm_tool()` 也支持类似的写法,通过 `permission_type` 参数为工具声明一个默认权限:

```py
from astrbot.api.event import filter, AstrMessageEvent

@filter.llm_tool(name="restart_server", permission_type=filter.PermissionType.ADMIN)
async def restart_server(self, event: AstrMessageEvent):
'''重启服务器。'''
# 处理逻辑
```

`permission_type` 可选 `filter.PermissionType.ADMIN` 或 `filter.PermissionType.MEMBER`,不传则保持原有行为(所有人可用)。

如果你是通过 `@dataclass` + `FunctionTool` 的方式定义 Tool(见上方[定义 Tool](#定义-tool)一节),也可以用同样的方式声明默认权限,只需要在 dataclass 里加上 `declared_permission_type` 字段:

```py
from pydantic import Field
from pydantic.dataclasses import dataclass

from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext


@dataclass
class RestartServerTool(FunctionTool[AstrAgentContext]):
name: str = "restart_server"
description: str = "Restart the server."
parameters: dict = Field(default_factory=lambda: {"type": "object", "properties": {}})
declared_permission_type: str | None = "admin" # 可选 "admin" / "member" / None

async def call(self, context: ContextWrapper[AstrAgentContext], **kwargs) -> ToolExecResult:
# 处理逻辑
return "ok"
```

> [!WARNING]
> - `permission_type` / `declared_permission_type` 只是工具的**默认权限**。如果机器人主人在 WebUI 面板(扩展 -> 组件 -> 工具管理)里为该工具手动配置过权限,面板上的配置会**覆盖**插件代码里声明的默认值。
> - 这个机制的意义在于:即便机器人主人从未打开过 WebUI 面板配置任何东西,插件作者依然可以为自己写的危险工具(例如重启服务、执行 shell 命令等)提供一层默认的安全防护,而不必依赖用户主动去配置。

## 调用 Agent

> [!TIP]
Expand Down
Loading
Loading