Appearance
权限系统 🔥
Agent 的威力来自能调用工具,而工具中最危险的是 Bash 与 Write--一条 rm -rf / 或一次对 ~/.bashrc 的覆写就足以毁掉整个环境。AgentScope 2.x 把"工具执行前的拦截"做成了一等公民: Permission 子系统在每一次工具调用真正执行前做一次三态决策(allow / deny / ask), 配合五种模式、可配置规则与 Bash 静态分析,构成 Agent 执行链上的安全闸门。
版本基线:本文以 Python
agentscope 2.0.4(PyPI,2026-07-07)为准,要求 Python>=3.11。 权限相关类型从agentscope.permission导入,公开符号为PermissionContext、AdditionalWorkingDirectory、PermissionDecision、PermissionEngine、PermissionRule、PermissionMode、PermissionBehavior(源:src/agentscope/permission/__init__.py)。2.x 文档站 存在版本化陷阱:非版本化的/en/URL 会 404,本页所有官方链接均使用versions/2.0.4/en/形式。
为什么需要权限系统
Agent 的 ReAct 循环会自主决定调用哪个工具、传什么参数。一旦模型生成了一条 Bash 工具调用, 默认行为就是直接执行--这在本地开发时或许可以接受,但在生产、多租户或无人值守场景下是灾难性的。 2.x 在工具执行前插入了一层 PermissionEngine 检查:它综合规则(PermissionRule)、模式 (PermissionMode)与工具自身的运行时分析(check_permissions / check_read_only),给出 allow / deny / ask 三种决策之一。ask 会暂停 Agent 并产出 HITL 事件,等待用户裁决。
前端类比
如果你来自前端,可以把权限系统类比为 Express 的 auth middleware + 三态闸门:
- auth middleware 决定请求"放行 / 拒绝 / 重定向到登录页",对应
allow/deny/ask。 - 模式 类似 middleware 的预设策略:开发模式放行所有请求(
BYPASS)、只读模式拒绝所有写操作 (EXPLORE)、生产模式默认拒绝未鉴权请求(DONT_ASK)。 - 规则 类似路由级 allowlist / denylist:
app.post('/api/*', allowIfRole('admin'))。
AgentScope 原生语义:权限系统不是 HTTP middleware,而是一个在工具调用同步路径上的决策引擎。 它检查的不是用户身份,而是"这个 Agent 此刻想用这个工具做这件事,是否被允许"。ask 不是重定向, 而是暂停整个 ReAct 循环,通过 RequireUserConfirmEvent 把决策权交还给人,恢复后再继续。这与 Express 的请求 / 响应模型完全不同:Agent 的执行是长生命周期的异步流,一次 ask 可能让 Agent 暂停数分钟乃至数小时。
三态决策流程
下面这张流程图说明一次工具调用如何穿过 PermissionEngine 得到三态决策。图前先说明:决策的输入 是工具名 + 实际调用参数 + 当前 PermissionContext;输出是 PermissionDecision,其 behavior 字段为 ALLOW / DENY / ASK 之一(PASSTHROUGH 是工具表示"无意见,交给引擎"的内部值, 不直接作为最终决策)。
图后解释关键路径:
- 规则优先:
PermissionRule的匹配优先级最高。命中DENY或显式ASK规则时直接拍板, 不再进入工具内置检查。DENY与ASK规则在所有模式下都生效,包括BYPASS。 - 工具内置检查:每个工具实现
check_permissions(),针对实际调用参数做运行时分析。例如Bash会解析命令字符串判断是否只读,Write会检查目标路径是否属于危险路径(.bashrc/.ssh/等)。 PASSTHROUGH是工具表示"我没有特殊意见"的内部值,引擎收到后会按当前PermissionMode兜底。bypass_immuneASK:工具认为过于危险、不应被静默放行的 ASK(如rm -rf /、写~/.bashrc)。 在DEFAULT/ACCEPT_EDITS/DONT_ASK下,即使有匹配的ALLOW规则也无法压制它;在DONT_ASK下被转为DENY;在BYPASS下被有意跳过。ask暂停与恢复:ask决策会让 Agent 产出RequireUserConfirmEvent并暂停。用户批准时 可以顺带接受suggested_rule,该规则会被持久化进引擎,后续同类调用自动放行不再询问。
五种模式
PermissionMode 是一个全局策略,决定"没有规则匹配时怎么办"。它可以在构造 Agent 时设定,也可以 运行时切换(见 运行时切换模式)。
| 模式 | 行为 | 何时用 |
|---|---|---|
DEFAULT | 所有操作都需要显式规则或用户确认。唯一自动放行的是 Bash 识别为只读的命令(ls / git status / cat 等) | 最安全,推荐默认 |
ACCEPT_EDITS | 自动放行工作目录内的文件操作;自动放行 Bash 文件系统命令(mkdir/touch/rm/cp/mv/sed),但要求每个目标路径都解析进工作目录 | 开发时有人在旁边盯着 |
EXPLORE | 只读:放行只读工具与只读 Bash 命令,拒绝所有修改。用户配置的 DENY / ASK 规则优先于只读自动放行 | 代码探索、规划阶段 |
BYPASS | 跳过所有权限检查,包括工具安全 ASK(rm -rf /、写 ~/.bashrc 等都会放行)。只有 DENY / ASK 规则和工具 DENY 仍然生效 | 沙箱环境或完全可信的运行 |
DONT_ASK | 把所有 ASK(默认 ASK、ASK 规则、安全 ASK)都转为 DENY;无交互场景的安全默认 | 无人值守 / 定时执行 |
⚠️
BYPASS不是"更宽松的DEFAULT":它有意跳过工具内置的安全 ASK。官方文档明确指出BYPASS下rm -rf /、写~/.bashrc、命令注入等都会直接放行。只有在运行环境本身是沙箱 (DockerWorkspace/E2BWorkspace)或完全可信时才应使用,且必须配合deny_rules保护 关键路径。无人值守场景应优先用DONT_ASK,它保留了安全网且永不弹窗。
规则系统
PermissionRule
PermissionRule 把"某个工具 + 某种调用模式"映射到一种行为。四个字段如下(源:官方 2.0.4 文档 与 src/agentscope/permission/_rule.py):
| 字段 | 类型 | 作用 |
|---|---|---|
tool_name | str | 规则适用的工具名:"Bash"、"Read"、"Write"、"Edit" 或任意自定义工具名 |
rule_content | str | None | 匹配模式,语义随工具变化(见下表) |
behavior | PermissionBehavior | ALLOW / DENY / ASK(PASSTHROUGH 是工具内部值,不用于规则) |
source | str | 规则来源:"userSettings"、"projectSettings"、"session" 等 |
rule_content 的匹配语义由每个工具的 match_rule() 方法决定,因此不同工具的模式语法不同:
| 工具 | 匹配对象 | 模式语法 | 示例 |
|---|---|---|---|
Bash | command 参数 | 通配前缀 PREFIX:* | npm run:* 匹配 npm run build,不匹配 npm install |
Read/Write/Edit | file_path 参数 | glob(fnmatch) | src/**/*.py 匹配 src/ 下任意 .py |
| 其他工具 | JSON 序列化参数 | 精确匹配 | 取决于工具实现 |
PermissionBehavior
PermissionBehavior 是一个枚举,有四个值。用户面向的是前三个,PASSTHROUGH 是工具内部值:
| 值 | 含义 |
|---|---|
ALLOW | 放行,工具立即执行 |
DENY | 拒绝,生成错误 ToolResult 回灌进上下文,模型可以看到 |
ASK | 暂停 Agent,产出 RequireUserConfirmEvent 等待用户裁决 |
PASSTHROUGH | 工具表示"无意见",交给引擎按模式兜底(不用于规则配置) |
AdditionalWorkingDirectory
AdditionalWorkingDirectory 声明一个 Agent 可以读写的额外目录,用在 ACCEPT_EDITS 模式下判断 文件操作是否落在允许范围内。两个字段:
| 字段 | 类型 | 作用 |
|---|---|---|
path | str | 目录绝对路径 |
source | str | 来源:"userSettings" / "session" 等 |
PermissionContext
PermissionContext 是规则的容器,通过 AgentState.permission_context 注入 Agent。它持有:
mode: PermissionMode- 当前模式。working_directories: dict[str, AdditionalWorkingDirectory]- 工作目录集合,键为路径。allow_rules: dict[str, list[PermissionRule]]- 按工具名分组的 allow 规则。deny_rules: dict[str, list[PermissionRule]]- 按工具名分组的 deny 规则。
DENY 与显式 ASK 规则在所有模式下都生效(包括 BYPASS),是真正的硬护栏。
构造带权限的 Agent
下面是一个完整构造示例:ACCEPT_EDITS 模式 + 工作目录 + allow / deny 规则。凭据通过环境变量 注入,不要写死在代码里。
python
import os
from agentscope.agent import Agent
from agentscope.state import AgentState
from agentscope.model import DashScopeChatModel
from agentscope.credential import DashScopeCredential
from agentscope.permission import (
PermissionContext,
PermissionMode,
PermissionBehavior,
PermissionRule,
AdditionalWorkingDirectory,
)
agent = Agent(
name="dev_agent",
system_prompt="你是一个能读写代码的助手,只在当前项目目录内操作。",
model=DashScopeChatModel(
credential=DashScopeCredential(
api_key=os.environ["DASHSCOPE_API_KEY"],
),
model="qwen-plus",
),
state=AgentState(
permission_context=PermissionContext(
mode=PermissionMode.ACCEPT_EDITS,
working_directories={
"/my/project": AdditionalWorkingDirectory(
path="/my/project",
source="userSettings",
),
},
allow_rules={
"Bash": [
PermissionRule(
tool_name="Bash",
rule_content="npm run:*",
behavior=PermissionBehavior.ALLOW,
source="userSettings",
),
],
"Write": [
PermissionRule(
tool_name="Write",
rule_content="src/**",
behavior=PermissionBehavior.ALLOW,
source="userSettings",
),
],
},
deny_rules={
"Bash": [
PermissionRule(
tool_name="Bash",
rule_content="rm:*",
behavior=PermissionBehavior.DENY,
source="userSettings",
),
],
},
),
),
)上面的配置含义:Agent 可以在 /my/project 内自由写文件;npm run * 命令自动放行;任何 rm 命令无论是否在工作目录内都被拒绝。ACCEPT_EDITS 模式下,工作目录内的 mkdir / touch / cp 等 Bash 文件系统命令也会自动放行,前提是每个目标路径都解析进 /my/project。
运行时切换模式
模式不是一成不变的。PermissionContext 挂在 AgentState 上,可以直接修改 mode 字段:
python
from agentscope.permission import PermissionMode
# 探索阶段:切到只读,Agent 只能看不能改
agent.state.permission_context.mode = PermissionMode.EXPLORE
# 用户确认方案后:切回可写模式开始改代码
agent.state.permission_context.mode = PermissionMode.ACCEPT_EDITS
# 批量执行时:切到无人值守模式,ASK 全部转 DENY
agent.state.permission_context.mode = PermissionMode.DONT_ASK运行时切换不会丢失已积累的规则--包括 HITL 中用户接受的 suggested_rule。这些规则会随 PermissionContext 一起持久化进 AgentState(见 状态与会话)。
Bash 静态分析与 bypass_immune
管道命令解析
Bash 工具内置静态分析(源:src/agentscope/permission/_bash_parser.py),在调用时解析命令字符串, 判断整体是否只读。规则是:
- 复合命令(
&&/||/;/|)只有当所有子命令都是只读时,整体才判为只读。ls && rm x不是只读,因为有rm。 - 输出重定向(
>/>>)会让命令变为非只读,即使原本是只读命令。ls > files.txt不是只读。 - 只读命令清单(部分):
ls、cat、head、tail、grep、rg、find、tree、stat、wc、pwd、which;git status/git log/git diff/git show/git branch/git blame;docker ps/docker images/docker logs/docker inspect;gh repo view/gh issue list;npm list/pip list/pip show。
只读命令在所有模式下(包括 DEFAULT)都自动放行,无需配置规则。这是 DEFAULT 模式下唯一的 自动放行路径。
bypass_immune 安全规则
工具在 check_permissions() 中可以返回带 bypass_immune=True 的 PermissionDecision,表示这个 ASK 过于危险、不应被静默压制。PermissionDecision 的关键字段:
| 字段 | 类型 | 作用 |
|---|---|---|
behavior | PermissionBehavior | 决策行为 |
message | str | 给人看的说明 |
decision_reason | str | 决策原因(内部记录) |
bypass_immune | bool | 为 True 时,此 ASK 在多数模式下无法被 ALLOW 规则压制 |
bypass_immune=True 的 ASK 在各模式下的处理(源:官方 2.0.4 文档):
| 模式 | bypass_immune=True ASK 的处理 |
|---|---|
DEFAULT | 生效,ALLOW 规则无法压制 |
ACCEPT_EDITS | 生效,同 DEFAULT |
EXPLORE | 不适用(引擎在 EXPLORE 下不调用 check_permissions,只读判定即终判) |
BYPASS | 有意跳过--BYPASS 的契约是"用户已放弃安全提示,只剩规则护栏" |
DONT_ASK | 转为 DENY(没有用户可以回答) |
典型场景:Write 工具检测到目标路径是 ~/.bashrc,返回 bypass_immune=True 的 ASK。在 DEFAULT 下即使用户配了 allow_rules["Write"] = ["**"] 也无法静默放行;在 BYPASS 下则被跳过, 因此 BYPASS 必须配合 deny_rules 保护这些路径。
与 HITL 的关系
ask 决策是权限系统与 HITL(Human-in-the-Loop)的交汇点。当 PermissionEngine 返回 ASK 时, Agent 会:
- 产出
RequireUserConfirmEvent,其中携带工具调用信息与自动生成的suggested_rules。 - 暂停 ReAct 循环,
reply返回当前已累积的Msg,reply_stream产出该 Require 事件。 - 等待用户通过
UserConfirmResultEvent恢复。用户可以:- 批准并接受
suggested_rule,该规则被持久化进引擎,后续同类调用自动放行。 - 批准但不接受规则(只本次放行)。
- 拒绝,工具生成错误
ToolResult回灌进上下文,模型据此调整策略。
- 批准并接受
完整的 HITL 代码模式(接收事件 -> 构造结果 -> 恢复)在 流式事件 页展开。本页只需记住:权限系统的 ask 是 HITL 的主要触发源之一,另一个来源是 is_external_tool=True 的外部工具(触发 RequireExternalExecutionEvent)。
常见错误
BYPASS模式滥用:把BYPASS当成"省去确认"的便捷开关。BYPASS会跳过工具内置安全 ASK,rm -rf /、写~/.bashrc、命令注入都会放行。只有在沙箱(DockerWorkspace/E2BWorkspace) 或完全可信环境才用,且必须配deny_rules。无人值守场景用DONT_ASK更安全。⚠️
DONT_ASK优于BYPASS用于无人值守:DONT_ASK保留工具安全网,只是把 ASK 转 DENY;BYPASS则完全放弃安全网。两者的"不弹窗"效果相同,但DONT_ASK的事故半径小得多。rule_content写错语法:Bash用PREFIX:*前缀通配,不是 glob;Write/Edit/Read用fnmatchglob。把npm run:*写成npm run*或npm run/*都不会匹配。正确写法是命令前缀:*,例如npm run:*匹配npm run build。误以为权限替代沙箱:权限是闸门,决定工具"能不能执行";沙箱是执行隔离,决定工具"在哪里 执行"。
BYPASS模式 +LocalWorkspace等于在没有隔离的机器上放行一切,权限的deny_rules是 唯一护栏。真正的纵深防御是DEFAULT/ACCEPT_EDITS+DockerWorkspace/E2BWorkspace, 详见 工作区与沙箱。期望
EXPLORE模式调用check_permissions:EXPLORE下引擎不调用工具的check_permissions,只看check_read_only的返回值。因此bypass_immuneASK 在EXPLORE下 不生效--只读判定就是终判。混淆
allow_rules与deny_rules:PermissionContext同时支持两者。deny_rules在所有 模式下(包括BYPASS)都生效,是硬护栏;allow_rules在DEFAULT下放行匹配调用,但无法压制bypass_immune=True的安全 ASK。把安全关键规则放进deny_rules,不要只靠allow_rules。HITL 接受规则后不持久化
AgentState:用户在 HITL 中接受的suggested_rule会进当前PermissionContext,但如果回复结束后没有把AgentState写回RedisStorage,下次会话规则就丢了。 持久化模式见 状态与会话。
先修
- 中间件 - 理解包裹执行链的洋葱模型,权限检查发生在中间件链之后、工具执行之前
下一步
- 工作区与沙箱 - 权限决定"能不能执行",Workspace 决定"在哪里执行"