Skip to content
Open
Changes from all 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
356 changes: 356 additions & 0 deletions docs/llm_tool_permission_model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
# LLM Tools 统一权限模型提案

## 背景

AstrBot 中插件 tools、MCP tools、Handoff tools 以及部分内置 tools 最终都会进入 LLM tool-calling 工具集合。当前第三方工具的权限控制主要依赖插件或工具开发者自行在 handler 内实现。

如果插件开发者忘记在工具内部检查管理员权限,普通用户可能通过自然语言诱导 LLM 调用高危工具。对于文件读写、命令执行、浏览器控制、MCP 外部能力、定时任务等工具,这会带来宿主机与账号安全风险。

## 当前问题

当前权限控制存在几个结构性问题:

1. 第三方插件工具没有统一鉴权入口。
2. MCP tools 默认缺少 AstrBot 侧统一权限模型。
3. LLM 请求构建阶段会把可用工具 schema 注入给模型,普通用户可能看到不应看到的高危工具 schema。
4. 执行阶段缺少对所有 LLM tools 的统一兜底校验。
5. WebUI 无法按工具维度调整 `member` / `admin` / `disabled` 权限。

因此,第三方 tool 是否安全主要取决于插件开发者是否主动写二次鉴权。

## 目标

希望 AstrBot 为所有 LLM tools 提供统一、可配置、向后兼容的权限模型。

建议权限级别:

```text
member 所有人可用
admin 仅 AstrBot 管理员可用
disabled 完全禁用,LLM 不可见,也不可执行
```

## 建议设计

### 1. 为 FunctionTool 增加 permission 字段

建议在 `FunctionTool` 中增加:

```python
permission: str | None = None
```

含义:

- `None`:工具未显式声明权限,走全局默认策略;
- `member`:所有用户可用;
- `admin`:仅管理员可用;
- `disabled`:完全禁用。

使用 `None` 而不是直接默认 `member`,可以让旧插件统一受全局默认策略控制,方便后续渐进式收紧。

### 2. 为 @llm_tool 装饰器增加 permission 参数

维护者提到当前 `@llm_tool` 装饰器应该还不支持 permission 参数。建议后续实现时为装饰器补充该能力,使插件开发者可以在声明工具时直接给出默认权限。

示例:

```python
@llm_tool(
name="write_sensitive_file",
desc="Write sensitive files",
permission="admin",
)
async def write_sensitive_file(event: AstrMessageEvent, path: str, content: str):
...
```

如果 `permission` 未传,则保持向后兼容:

```python
@llm_tool(name="query_info", desc="Query public info")
async def query_info(event: AstrMessageEvent, keyword: str):
...
```

此时工具权限为 `None`,最终由全局 `undeclared_default` 决定。

建议装饰器侧只负责把声明值写入对应 `FunctionTool.permission`,最终是否生效仍由统一权限决策模块计算。

### 3. 增加统一权限决策模块

建议新增类似模块:

```text
astrbot/core/agent/tool_permission.py
```

职责:

- 标准化权限值;
- 生成稳定 permission key;
- 解析工具最终生效权限。

建议 key 格式:

```text
builtin:{tool_name}
plugin:{handler_module_path}:{tool_name}
mcp:{server_name}:{tool_name}
unknown:{tool_name}
```
Comment on lines +94 to +101
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.

medium

建议与改进:处理 handler_module_path 为空的情况

在生成插件工具的 permission key 时,使用了 plugin:{handler_module_path}:{tool_name} 格式。

潜在问题:
FunctionTool 中,handler_module_path 是一个可选字段(str | None = None)。如果插件开发者是通过手动注册或动态构建的方式添加工具,该字段可能会为 None。这会导致生成的 key 变成 plugin:None:{tool_name},在存在多个此类工具时会产生冲突。

建议:
建议在设计中明确规定当 handler_module_pathNone 时的兜底策略。例如:

  1. 优先尝试获取插件的注册名称/ID。
  2. 如果完全无法获取,则退化为 unknown:{tool_name},或者在注册时强制要求或自动填充一个唯一的模块标识符。


其中插件工具的 key 生成需要明确兜底策略:

1. 优先使用 `handler_module_path`。
2. 如果 `handler_module_path` 为空,尝试使用插件注册名、插件 root dir 或 Star metadata 中可稳定识别插件身份的字段。
3. 如果仍然无法确认来源,则退化为 `unknown:{tool_name}`,而不是生成 `plugin:None:{tool_name}`。
4. 对 `unknown` 来源工具,WebUI 应明确展示其来源未知,管理员可以手动调整权限,但实现侧应避免多个未知来源同名工具互相覆盖。

权限优先级:

1. WebUI 覆盖配置;
2. 工具自身声明;
3. 全局未声明工具默认策略。

### 4. 权限配置存储位置

工具权限不建议存放在 `provider_settings` 内。

原因是工具权限与具体 LLM Provider 无关。用户切换 OpenAI、Claude、Ollama 或其它模型提供商时,不应该导致工具权限配置变化或需要重新配置。

建议将工具权限配置放在全局配置的独立字段中,例如:

```json
{
"llm_tool_permission_settings": {
"undeclared_default": "member",
"overrides": {
"mcp:playwright:browser_navigate": "admin",
"plugin:plugins.example.main:write_file": "admin",
"builtin:future_task": "admin"
}
}
}
```

如果 AstrBot 需要支持不同 UMO / channel / workspace 使用不同配置,则建议沿用现有 `get_config(umo=...)` 的配置解析方式,在每个会话配置中保存自己的 `llm_tool_permission_settings`。

也就是说:

- 配置字段不归属于 provider;
- 权限解析可以按当前 UMO 读取对应配置;
- permission key 本身保持稳定,不携带 UMO;
- 覆盖表的来源由当前会话配置决定,避免多租户 / 多空间场景下来源不清。

### 5. 请求阶段过滤 tool schema

在 LLM 请求真正发出前,根据当前事件用户权限过滤 `req.func_tool`。

建议位置:

```text
_plugin_tool_fix(event, req)
之后
AgentRunner.reset / provider request 之前
```
Comment on lines +148 to +156
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.

medium

建议与改进:避免过滤 req.func_tool 时的副作用与竞态条件

在请求阶段过滤 req.func_tool 时,需要特别注意并发安全和副作用。

潜在问题:
如果 req.func_tool 指向的是一个共享的全局 ToolSet 实例,直接在请求阶段对其进行 in-place 过滤(修改其内部的 tools 列表)会导致严重的竞态条件(Race Condition)。例如,一个普通用户的请求可能会意外地把管理员用户的可用工具也过滤掉,或者反之导致越权。

建议:
建议在文档中明确指出:过滤操作必须在 ToolSet 的深拷贝(Deep Copy)或请求局部副本上进行。确保每个请求的 req.func_tool 都是独立的实例,从而彻底避免并发修改带来的安全隐患。


期望行为:

- `disabled` 工具永远不注入给 LLM;
- 非管理员用户看不到 `admin` 工具 schema;
- 管理员可以看到 `member` 和 `admin` 工具;
- 从源头降低 prompt injection 和 schema 暴露风险。

过滤时必须避免原地修改共享 ToolSet。

建议实现要求:

- 不要直接修改全局 `provider_manager.llm_tools.func_list`;
- 不要原地裁剪可能被多个请求共享的 `ToolSet.tools`;
- 应构造请求局部 `ToolSet` 副本,或在过滤前进行深拷贝;
- 最终仅替换当前请求的 `req.func_tool`。

这样可以避免普通用户请求过滤工具时影响管理员请求,或并发请求之间互相污染。

### 6. 执行阶段二次兜底校验

在 `ToolLoopAgentRunner._handle_function_tools()` 中,在构造 `valid_params` 与真正执行工具前增加权限检查。

这样即使出现:

- 历史上下文残留;
- 请求层过滤遗漏;
- 外部伪造 tool_call;
- 工具 schema 模式切换;

执行层仍然可以兜底拒绝未授权调用。

建议行为:

- `disabled`:永远拒绝;
- `admin + 普通用户`:拒绝;
- `admin + 管理员`:允许;
- `member`:允许继续执行;
- 无法确认上下文安全性时:拒绝。

### 7. 系统上下文的安全识别

无 `event` 的系统任务 / cron / 后台任务不应简单视为普通用户,否则可能误伤内部系统上下文。

但也不应简单通过 `event is None` 就放行 admin 工具,否则未来如果存在可被外部构造的无事件调用路径,可能形成绕过。

建议引入明确的受信任系统上下文机制,例如:

```python
trusted_system_context: bool = False
```

或类似不可由用户输入伪造的内部标志,例如 `TrustedToolContext` / `SystemContextToken`。

建议规则:

1. 有 `event` 时,严格使用 `event.is_admin()` 判断。
2. 无 `event` 时,只有当 run context 显式带有内部可信标志,才视为系统上下文。
3. 该可信标志只能由 Cron、系统主动任务、内部 runner 等框架代码设置,不能从用户 prompt、tool args 或外部请求参数中读取。
4. 如果既没有 `event`,也没有可信系统标志,则拒绝执行 `admin` 工具。

这样既能避免误伤系统任务,又能避免把 `event is None` 变成权限绕过条件。

### 8. 与工具内部鉴权的关系

统一权限模型不应替代工具 handler 内部的业务校验。

维护者也提到,Tool 的 `call` 方法中通常可以拿到 event,开发者仍然可以通过 `event.is_admin()` 判断用户是不是管理员。因此文档中需要明确说明:统一权限是框架兜底,不是禁止工具自己继续鉴权。

建议采用“最小权限优先 / 任一拒绝即拒绝”的策略:

- 统一权限模型负责 LLM schema 可见性与执行前基础权限兜底;
- 工具 handler / Tool.call 内部仍可通过 event 做业务级权限检查;
- 如果统一权限允许,但 handler 内部拒绝,则最终拒绝;
- 如果统一权限拒绝,则不应进入 handler;
- 对高危工具,建议同时保留 handler 内部鉴权作为纵深防御。

示例:

```python
async def call(self, context: ContextWrapper[AstrAgentContext], **kwargs):
event = context.context.event
if not event.is_admin():
return "error: permission denied"
...
```

也就是说,统一权限模型是框架级最低安全线,工具内部鉴权是业务级补充安全线。

### 9. 高危框架工具默认收紧

建议默认标记为 `admin`:

- Handoff tools / sub-agent transfer tools;
- MCP tools;
- FutureTaskTool / cron 管理工具;
- 文件改写、shell、浏览器控制等高危工具。

旧插件为了兼容可以默认 `member`,但建议 WebUI 中清晰展示未声明权限的工具,并允许管理员手动调整。

### 10. WebUI 提供工具权限管理和 override 接口

希望 WebUI 工具管理页展示:

- 工具名;
- 来源:builtin / plugin / mcp / unknown;
- 当前有效权限;
- permission key;
- 是否启用;
- 可编辑权限:`member` / `admin` / `disabled`。

修改后持久化到 `llm_tool_permission_settings`,而不是 provider 专用配置。

示例:

```json
{
"llm_tool_permission_settings": {
"undeclared_default": "member",
"overrides": {
"mcp:playwright:browser_navigate": "admin",
"plugin:plugins.example.main:write_file": "admin",
"builtin:future_task": "admin"
}
}
}
```

维护者建议可以在前端加上 override 的权限接口。建议后端提供一个专门接口,例如:

```text
POST /tools/set-permission
```

请求体示例:

```json
{
"permission_key": "mcp:playwright:browser_navigate",
"permission": "admin"
}
```

接口行为:

1. 校验 `permission_key` 非空;
2. 校验 `permission` 必须是 `member` / `admin` / `disabled`;
3. 写入当前配置的 `llm_tool_permission_settings.overrides`;
4. 持久化配置;
5. 返回更新后的有效权限。

WebUI 可以基于 `/tools/list` 返回的 `permission` 和 `permission_key` 渲染下拉框,用户修改后调用该接口保存 override。

### 11. 开发者文档补充

建议新增或更新插件开发文档,明确说明 LLM Tool 权限问题。

文档应包含:

1. `@llm_tool(permission="admin")` 的用法;
2. 未声明权限时走全局默认策略;
3. 高危工具建议声明为 `admin`;
4. Tool 的 `call` 方法或 handler 内可以通过 event 判断用户身份;
5. 统一权限模型和工具内部鉴权的关系;
6. WebUI override 会覆盖工具默认声明。

示例文档片段:

```python
@llm_tool(name="dangerous_action", desc="Run dangerous action", permission="admin")
async def dangerous_action(event: AstrMessageEvent, arg: str):
if not event.is_admin():
return "error: permission denied"
...
```

这样可以让插件开发者既知道如何声明默认权限,也知道在业务逻辑中继续保留必要的细粒度鉴权。

## 预期收益

- 第三方插件即使遗漏鉴权,也有后端统一兜底;
- 普通用户不会看到高危工具 schema;
- MCP、插件、内置工具可以统一治理;
- 管理员可以在 WebUI 中动态调整工具权限;
- 旧插件保持兼容,未声明权限时默认按全局策略处理;
- 后续可以逐步将高危工具默认上调为 `admin`。

## 兼容性建议

MVP 阶段可先做:

1. `FunctionTool.permission: str | None = None`;
2. `@llm_tool` 支持 `permission` 参数;
3. 新增 `tool_permission.py`;
4. 执行层权限兜底;
5. WebUI 工具列表返回 `permission` / `permission_key`;
6. WebUI / 后端提供 override 权限接口;
7. 插件文档补充权限声明和 handler 内鉴权说明。

请求层过滤和高危工具默认收紧可以作为后续迭代补齐,也可以在 MVP 后逐步启用。