跳到主要内容

03 · 权限引擎与人机交互

这一章是 AgentScope 的安全核心。一个会自己跑命令、改文件的 agent,凭什么让人放心?答案是:每次工具执行前都过一道与循环解耦的权限闸,而且这道闸能在判定「要问用户」时把整个循环干净地暂停。

3.1 它要解决的小问题

模型可能要求执行 rm -rf /、改你的 ~/.bashrc、把代码推到生产。你不能无脑放行,也不能每条命令都打断你问一遍(那太烦)。需要一套可配置的策略:哪些自动放行、哪些直接拒、哪些必须当场问你。

3.2 思路/直觉:三层判定源 × 五种模式

权限判定的三个信息源:

  1. 用户配置的规则(allow / deny / ask rules)——比如「凡是 git:* 命令一律放行」。
  2. 工具自己的判断(tool.check_permissions)——工具最懂自己危不危险,比如 Bash 自动放行 ls/git status 这类只读命令,但对 rm -rf / 发出安全 ask
  3. 模式(PermissionMode)——同样的工具调用,在「探索模式」和「绕过模式」下命运完全不同。

PermissionEngine.check_permission(permission/_engine.py:76)是总分发器,按当前模式跳到对应的 _check_<mode> 方法。每种模式有自己独立、可单独阅读的判定顺序——这是刻意的设计(permission/_engine.py:16 类注释),免得把五种策略糊成一个巨型 if-else。

3.3 五种模式各自的判定顺序

模式一句话适用场景
DEFAULT默认啥都问,除非 allow 规则命中或工具自己放行最安全,默认
ACCEPT_EDITS工作目录内的读写自动放行,其余走常规有人盯着、快速迭代开发
EXPLORE只读:只读操作放行,任何修改一律拒探索代码库、做规划
BYPASS跳过所有安全 ask,只剩用户的 deny/ask 规则当护栏沙箱/容器里、完全信任 agent
DONT_ASK把每个 ask(含安全 ask)都转成 deny无人值守的定时/后台任务

DEFAULT 模式(permission/_engine.py:116)为例,判定顺序是:

1. deny 规则命中? ─是─▶ DENY (最高优先级)
2. ask 规则命中? ─是─▶ ASK
3. 工具 check_permissions
├─ ALLOW / DENY ────▶ 原样返回
└─ 安全 ASK(bypass_immune)─▶ ASK(allow 规则也压不住)
4. allow 规则命中? ─是─▶ ALLOW
5. 都没命中 ────▶ ASK(默认问用户)

对比着看几个模式的「精华差异」:

  • EXPLORE(permission/_engine.py:189):第 3 步直接用 tool.check_read_only(tool_input) 一锤定音——只读就 ALLOW,否则 DENY。它根本不调 check_permissions,也不查 allow 规则——因为「探索 = 只读」这个保证不能被任何用户规则放水。
  • ACCEPT_EDITS(permission/_engine.py:251):比 DEFAULT 多一条「只读快路放行」,且工具的 check_permissions 会对「工作目录内的写」返回 ALLOW(所以你在项目目录里改文件不会被反复打断)。
  • BYPASS(permission/_engine.py:337):第 3 步故意只认 ALLOW/DENY,无视任何 ASK(包括安全 ASK),兜底是 ALLOW。这是「我完全信任,别烦我」的契约。
  • DONT_ASK(permission/_engine.py:412):不变量是「永不返回 ASK」——没人在线答。每条本该 ASK 的路径都用 _convert_ask_to_deny(permission/_engine.py:487)转成 DENY,但保留原因和建议规则,方便事后告诉用户「加哪条规则就能解锁」。

3.4 巧妙之处:bypass-immune 安全门

工具对真正危险的操作(写 ~/.bashrcrm -rf /、命令注入)发出的不是普通 ASK,而是 bypass_immune=True 的 ASK(permission/_decision.pyPermissionDecision.bypass_immune)。它的语义是:「这件事危险到没有任何 allow 规则能把它静默放行,必须当场问。」

各模式对它的处理(permission/_engine.py:527_is_safety_ask):

模式对 bypass-immune ASK 的处理
DEFAULT / ACCEPT_EDITS尊重:allow 规则压不住,照样问用户
EXPLORE不适用(已被更宽的 DENY 吸收)
BYPASS故意忽略:契约就是「用户已opt-out安全提示」
DONT_ASK转成 DENY(没人能答)

这套设计的精妙处:把「危险程度」编码进决定本身,而不是散落在各模式里。工具只管标「这是安全 ASK」,引擎按模式决定怎么对待——职责清晰。

3.5 工具怎么参与判定

ToolBase(tool/_base.py:94)给每个工具几个可重写的判定钩子:

  • check_permissions(抽象,必须实现)——返回 ALLOW/DENY/ASK/PASSTHROUGH。PASSTHROUGH 意为「我不决定,交给引擎继续按规则匹配」。
  • check_read_only(tool_input)(tool/_base.py:258)——输入相关的只读判定。默认返回静态的 is_read_only,但 Bash 重写它:Bash 静态上不算只读,但 ls -a 这次调用其实是只读的。
  • match_rule / generate_suggestions——规则匹配与「建议规则」生成。比如 Bash 把 git commit -m '...' 建议成 git commit:*,文件工具把 src/main.py 建议成 src/**(tool/_base.py:315)。这些建议随 RequireUserConfirmEvent 发给用户,用户点「以后都允许」就把建议规则加进引擎。

_base.py 还内建了危险路径识别:_is_dangerous_path(tool/_base.py:402)对 .bashrc/.ssh/.git 等做大小写不敏感匹配,_path_in_allowed_working_path(tool/_base.py:350)用 realpath 比对(处理 macOS /tmp/private/tmp 这类软链)。

3.6 human-in-the-loop:循环怎么暂停又续上

这是把权限和「01 章的状态机」结合起来看的地方。当权限判定是 ASK,_execute_tool_call(agent/_agent.py:1416)做三件事:

  1. 把工具调用状态置为 ASKING(先改状态再发事件,顺序重要)。
  2. RequireUserConfirmEvent(带建议规则)。
  3. return——这一路不再往下执行。

外层 _reply_impl 检测到这个事件就停掉后续批次、yield 一个占位 AssistantMsg、return。整个循环就这样干净地暂停了,控制权回到调用方。

工具调用状态机(message/_block.py:104ToolCallState,docstring 里画了完整转移图)是续跑的关键:

pending
├─ 权限 DENY / 输入校验失败 ─▶ finished
├─ 权限 ASK ────────────────▶ asking
│ ├─ 用户拒 ─────────────▶ finished
│ └─ 用户准 ─────────────▶ allowed
└─ 权限 ALLOW ──────────────▶ allowed
allowed
├─ 本地工具 (执行) ──────────▶ finished
└─ 外部工具 ────────────────▶ submitted
submitted
└─ 收到外部执行结果事件 ─────▶ finished

用户答复后,再次调 reply_stream,这次传进来的是 UserConfirmResultEvent_check_incoming_event(agent/_agent.py:909)先校验「确实在等这个确认、id 对得上」,_handle_incoming_event(agent/_agent.py:1000)据此把被确认的工具调用置为 allowed(或拒绝则置 finished 并回拒绝结果)。然后循环顶部的 _check_next_action 一看「有 allowed 的工具调用」→ 返回 acting → 从中断处接着执行。模型完全不知道中间停过——上下文连续。

3.7 关键细节 / 坑

  • 状态更新必须在 yield 事件之前。 代码反复强调这点(agent/_agent.py:14211461),因为外层循环收到暂停事件后会立刻 break,yield 之后的代码根本不会执行。
  • 部分确认不重发。 如果一批工具调用只确认了一部分,agent 不会为未确认的那些重发 require 事件(agent/_agent.py:202 的 NOTE)。
  • stop_on_reject react_config.stop_on_reject(agent/_config.py:134)控制工具被拒后是否停止整个 reply。

3.8 小结

权限是 AgentScope「能放心上线」的支柱:独立引擎 + 五模式 + 工具自检 + bypass-immune 安全门,加上靠工具调用状态机实现的干净暂停/续跑。这套机制和主循环、事件流三位一体。

→ 下一章:04 · 中间件与上下文深入