权限模式与规则

工具调用前谁来拍板:三种权限模式的真实语义、规则写在哪与怎么写、询问怎样弹到 TUI、“始终允许”记在哪,以及 Bash 命令的分层判定。

作者 David更新于 第 17 篇(共 33 篇)

模型每发起一次工具调用,都要先过权限这一关:放行、拒绝,或者停下来问用户。MiniMax Code 把这件事拆成三层。packages/agent-modules/permission(包名 @mavis/permission)是纯决策引擎,包描述写明它从不 import 运行时包;packages/local-runtime/src/permissions 管规则存储,并按权限模式处理“询问”;v2 的 LocalTurnPermissionGate 把判定挂进 pi 的工具钩子,把询问交给审批服务,再推到 TUI。

和 OpenCode 的权限系统那种“默认全放行、最后一条匹配说了算”的规则模型相比,这里默认更保守,判定也分了更多层。

怎么用

TUI 里有三种模式(packages/tui/src/application/permission-mode.ts:1),配置里还多一个 off:

配置值TUI 名称行为
defaultAsk判定为询问就弹审批框
autoAuto询问先交给云端分类器,它放行就不问;没登录 MiniMax 账号时退化为 Ask
bypassPermissionsFull access所有询问直接放行、不弹框;硬性拒绝照旧
off不显示跳过整条判定,只保留删除改写
  • 切换:Alt+M 在 Ask、Auto、Full access 之间循环(permission-mode.ts:35,packages/tui/src/tui/shell/keybindings.ts:414),也可以用 /permission status|ask|auto|full(packages/tui/src/tui/commands/catalog.ts:293)。切换写进配置文件的 permissionMode(packages/local-runtime/src/api/host.ts:1493),全局、立即生效。缺省值是 auto(packages/config/src/config.ts:1708),读不懂的值也按 auto 处理(packages/local-runtime/src/api/host-helpers.ts:543)。状态栏显示当前模式,Full access 用警示色(packages/tui/src/tui/shell/chrome.ts:586)。TUI 读模式时只认前三种,配置为 off 时状态栏只显示笼统的 Permissions,按 Alt+M 只会提示“Permission mode is managed by Desktop”(packages/tui/src/tui/controller/interaction/permission-mode-flow.ts:54)。
  • headless:mcode exec --permission 取 smart(默认)、full、off,分别对应 auto、bypassPermissions、off(packages/tui/src/cli/run-exec-command.ts:129),只覆盖本次运行(packages/tui/src/runtime/lifecycle.ts:385)。headless 宿主不声明审批能力(lifecycle.ts:452),真走到需要问人的地方,审批请求会以 HOST_CAPABILITY_UNAVAILABLE 失败,模型收到的是一条工具错误(packages/local-runtime/src/api/hosted-agent-permission-capabilities.ts:93)。
  • 审批框:三个选项是“1 Allow for this conversation”、“2 Always allow matching actions”、“3 Deny and guide MCode”(packages/tui/src/tui/features/interaction/permission-picker.ts:82)。选“始终允许”会先列出将要保存的规则请你确认;选“拒绝”可以附一句指导,TUI 把它当成一条新消息发出(packages/tui/src/tui/app.ts:248)。/allow、/always、/deny 是不出现在命令列表里的备用写法。
  • permission 配置段(packages/config/src/permission-config.ts:46):policyOwner 选最终裁决者,默认 core,写别的值都回退到旧的 engine(:67);storageWriteVersion 选规则文件格式,默认 2;classifierTimeoutMs 与 userConfirmationEnabled 在公开代码里只被解析,门面创建分类器客户端时并不传入超时,固定为 60 秒(packages/local-runtime/src/permissions/facade.ts:419)。

引擎的结构

@mavis/permission 共 45 个文件、16599 行,按职责分成六块:

部分行数内容
Bash 判定7593tools/bash-*.ts 等 15 个文件:命令拆分、包装剥离、写目标、慢命令、首词快速放行、规则匹配
路径判定2814fs-permission.ts、path-capability.ts、fs-checker.ts 等:工作区边界、敏感文件、路径规则
核心2674engine.ts、permission-core.ts、context.ts、types.ts、execution-plan.ts 等
危险模式1754classifier/ 目录:HARD、SOFT 两套危险模式正则,外加是否走云端分类的判断
本地化与宿主接口1161原因文案、语言检测、configurePermissionHost 与包入口
云端网关与询问策略603Auto 模式调用的网关客户端、对话摘要、modeToAskPolicy

引擎只给两种工具注册了检查器:bash 用 BashToolPermissionChecker,edit、write、append、read、glob、grep、list 共用 FsToolPermissionChecker(packages/agent-modules/permission/src/tools/index.ts:14)。别的工具走路径能力的通用判断,最后由 permission-core.ts 的 reducePermissionDecision 统一裁决(packages/agent-modules/permission/src/engine.ts:213)。

一次调用的审批链

图表加载中…

挂载点:v2 在 pi 的 beforeToolCall 上排了一串钩子,权限闸门排在最后,前面依次是插件的 PreToolUse、Plan 模式与目标预算的策略守卫、“禁用全部工具”开关和扩展钩子(packages/local-runtime-v2/src/service/turn-system/agent-host/execution/plugin-hook-tool-lifecycle.ts:82)。钩子返回 block 时,pi 不执行工具,直接把原因作为错误结果交给模型(third_party/pi-mono/packages/agent/src/agent-loop.ts:720);闸门给的原因以 Permission denied for 或 Permission denied by user for 开头(packages/local-runtime-v2/src/service/turn-system/agent-host/runner/policy/local-turn-permission-gate.ts:701)。packages/agent-extension/src/permission.ts:118 另有一个通用的 permissionExtension,但生产装配里没有调用它。

闸门(local-turn-permission-gate.ts:171):source 为 builtin、builtin-matrix 的产品内置工具(如 Plan 模式的 EnterPlanMode)不过这道闸:只有宿主类型是 cli 时才检查它们(local-turn-permission-gate.ts:573,开关来自 packages/local-runtime-v2/src/service/turn-system/agent-host/native-production-dependencies.ts:229),而命令行版的 TUI、exec、ACP 共用一个嵌入宿主,类型一律报 tui(packages/tui/src/runtime/embedded-host.ts:84)。read、bash 这些本地工具没有 source 标记,照常检查。插件的 PermissionRequest Hook 能在弹框之前代答或改写参数,改写后要重新判定一次(local-turn-permission-gate.ts:425)。

门面(facade.ts:449):权限模式在入口只读一次,整条判定用同一份快照,免得用户中途切模式造成前后不一致。主体在 checkPermissionRaw 里:

  1. Windows 与 POSIX 的删除安全检查先行;Off 模式在此直接放行(facade.ts:556),除非命令含删除、需要改写。
  2. acceptEdits 是别名,按 default 处理,启动时为 edit、write、apply_patch 补上全局放行规则(facade.ts:566、:918)。
  3. 读出全局、Agent、会话三层规则,外加插件 Hook 授予的规则;规则文件损坏时要求询问,Full access 下则直接拒绝(facade.ts:598)。
  4. 本地硬检查:访问 UNC 网络共享、递归删除 / 或 ~,在 Ask、Auto、Full access 下都是绕不过的拒绝(facade.ts:628)。
  5. 引擎判定,再按模式处理询问(facade.ts:770):Full access 原样返回、由最后一步改成放行;Ask 直接询问;Auto 在托管运行时(已登录)把请求送到 /mavis/api/v1/permission/check(packages/agent-modules/permission/src/classifier/cloud-classify-client.ts:51),分类器说放行就放行,说拦截、要确认或超时都转成询问(facade.ts:856)。另有一个 dontAsk 模式把询问一律变成拒绝(facade.ts:774),它不在 TUI 和配置里,只能由插件 Hook 的 setMode 设置(packages/local-runtime/src/permissions/plugin-hook-permission-state.ts:73)。

第 5 步里“Full access 不弹框”这条产品承诺收在一个函数里(packages/local-runtime/src/permissions/ask-gate.ts:66):

export function applyAskGate(
  verdict: LocalPermissionCheckResult,
  mode: PermissionMode,
): LocalPermissionCheckResult {
  if (mode !== 'bypassPermissions') return verdict;
  if (verdict.behavior !== 'ask') return verdict;
  // Preserve `rewrittenInput` (e.g. `rm` → `mavis-trash` rewrite) so
  // the downstream tool call still receives the safer payload. Drop
  // `ruleContents` — they are an ask-card UI concern and have no
  // meaning on an allow verdict.
  return {
    behavior: 'allow',
    reason: BYPASS_ALLOW_REASON,
    ...(verdict.rewrittenInput ? { rewrittenInput: verdict.rewrittenInput } : {}),
  };
}

它只把询问改成放行,拒绝原样保留。所以 Full access 下仍会被拦的有:用户写的 deny 规则、HARD 最终拒绝类命令、上面的本地硬检查,以及后面会讲到的慢命令和 rm 以外的删除类命令。

Core 的裁决顺序(packages/agent-modules/permission/src/permission-core.ts:159):整工具或整个 MCP 服务的 deny 规则最先;检查器给出的 deny 其次,而且压过更宽泛的整工具 ask 规则;然后是整工具 ask、MCP 服务 ask、检查器出错、需要交互的工具;接着是检查器标记为 bypassImmune 的询问、内容级 ask 规则、检查器的普通询问;之后 Full access 放行;再往下才是整工具 allow 规则、检查器放行;没注册检查器的工具默认放行,其余一律询问。

Bash 命令怎样判定

evaluateBashStatic(packages/agent-modules/permission/src/tools/bash-permission.ts:727)先对整条命令做几项检查,因为这些形状会跨越 ;、| 等分隔符:

  • 字面上的 rm -rf /、rm -rf ~、rm -rf * 和 fork 炸弹直接拒绝,任何模式都一样(bash-permission.ts:796,清单在 packages/agent-modules/permission/src/classifier/dangerous-patterns.ts:118);
  • 剥开 bash -c、sudo 等包装后命中 HARD 最终拒绝类别的,直接拒绝(bash-permission.ts:827);这些类别包括不可恢复删除、磁盘擦除、勒索特征、数据外传等(dangerous-patterns.ts:190);
  • 非 Full access 时,curl … | bash 这类管道进 Shell 的形状要询问,但本地临时文件、工作区脚本和 python3 -c '…' 这类内联字面量可以豁免(bash-permission.ts:877);
  • 只带 --help、--version 之类参数的命令直接放行(bash-permission.ts:1004)。

然后按 &&、||、;、| 拆成子命令逐个判定,汇总时拒绝优先,其次询问,全部放行才放行(bash-permission.ts:1049)。每个子命令按下表的顺序走,先命中者定案:

顺序层结果出处
1用户 deny 规则拒绝bash-permission.ts:1144
2用户 ask 规则询问:1155
3HARD 最终拒绝类别拒绝:1172
3.5无界的全盘扫描,如不带 -maxdepth 的 find /拒绝,并提示改用更窄的命令:1192
—Full access 到此为止rm 改写为可恢复删除;其他含删除动词的命令拒绝;其余放行:1230
4HARD 敏感读取,如读 /etc/shadow询问:1297
5用户 allow 规则放行:1312
5.5写入目标在工作区等已授权位置放行:1328
6按子命令分级的危险模式,如 sudo询问:1341
6b读取环境里的密钥,如 printenv 某个凭据询问:1360
6.5source 本地临时文件或工作区脚本放行:1390
7SOFT 预扫描询问:1402
8rm放行并标记为可恢复删除:1444
9首词是只读命令且没有重定向放行:1451
尾都没命中询问:1471

函数头注释(bash-permission.ts:704)列的层次比代码少了 3.5、5.5、6b 三层。注释里还有一个 auto 模式,会把询问标成 undecided 留给云端分类器;但检查器只以 default 或 bypass 调用这个函数(packages/agent-modules/permission/src/tools/bash-checker.ts:655、:662),Auto 模式下 bash 与 Ask 判得一样,得出的询问再由门面送去分类器。

rm 在第 8 步“放行”,是因为删除已经在执行层变得可恢复:macOS 与 Linux 上 bash 的 PATH 里有一个 rm 垫片,把删除转给 mavis-trash,见文件与 Shell 工具;Windows 上检查器把命令改写成 mavis-trash -- 交给宿主执行(bash-checker.ts:922)。

包装与快照提交的修复

用户规则会同时匹配原命令和剥掉“透明包装”后的命令。透明包装指 xargs、nohup、setsid、command、nice、timeout、parallel、watch,最多剥四层,前面的 KEY=VAL 环境变量前缀也会去掉;sudo、exec、bash -c 不算,因为它们改变了权限或进程语义(packages/agent-modules/permission/src/tools/bash-wrapper-unwrap.ts:17、:50、:302)。

快照提交(#167,标题里的“preserve bash permission policy”)改的正是这里。修复前,只有第 5 步的 allow 规则会看剥掉包装后的命令,第 1、2 步的 deny、ask 只看原文。按当时的代码推演:规则同时有 allow git:* 和 deny git push:* 时,timeout 60 git push 在第 1 步没命中 deny,到第 5 步却被 allow 放行。修复后三类规则共用同一个匹配函数(bash-permission.ts:1136):

  // Every rule behavior sees the same raw and transparent-wrapper-stripped
  // forms. Otherwise a wrapped command could skip deny/ask and match allow.
  const wrapperStrippedCommand = stripTransparentWrappersForRuleMatch(command);
  const matchesUserRule = (parsedRule: CompiledShellPermissionRule | undefined): boolean =>
    !!parsedRule &&
    (matchShellRule(command, parsedRule) ||
      (!!wrapperStrippedCommand && matchShellRule(wrapperStrippedCommand, parsedRule)));

同一提交还补上了 cloneContextWith 漏抄的 isFile(packages/agent-modules/permission/src/context.ts:227),这个宿主提供的文件探针用于判断 source 的目标是不是真实的本地文件;不过公开仓库里没有调用 applyPermissionUpdate 的地方,这处遗漏在命令行版里碰不到。

规则写在哪、怎么写

规则分三层,各是一份 permission.json(packages/local-runtime/src/permissions/rules.ts:322):全局在数据目录根下,Agent 级在 agents/<名字>/,会话级在 sessions/<会话 ID>/。命令行版的数据目录默认是 ~/.minimax-code(packages/tui/src/runtime/data-dir.ts:21)。判定时三层合并读取(rules.ts:79)。

文件有两种格式(packages/local-runtime/src/permissions/rule-codec.ts:13)。v1 是字符串数组,工具名(内容) 的写法,如 bash(git:*);v2 是默认写入格式,每条规则带结构化的匹配器,kind 取 tool、command、path,路径规则还要列出适用的动作。读取时两种都认,v1 里以 /、~/、./ 或盘符开头的内容被当成路径(rule-codec.ts:276)。

下面是一份示意的 v2 文件:

{
  "version": 2,
  "allow": [
    { "tool_name": "bash", "matcher": { "kind": "command", "pattern": "git:*" } },
    { "tool_name": "fs", "matcher": { "kind": "path", "pattern": "/Users/me/notes/**", "actions": ["read", "write"] } }
  ],
  "deny": [{ "tool_name": "bash", "matcher": { "kind": "command", "pattern": "git push:*" } }],
  "ask": [{ "tool_name": "web_fetch", "matcher": { "kind": "tool" } }]
}
  • Bash 规则(packages/agent-modules/permission/src/tools/bash-rule-match.ts:71):git status 精确匹配;git:* 匹配 git 本身或以 git 开头的命令;含 * 的按通配符匹配、不区分大小写;JSON 数组如 ["cargo","build"] 要求命令解析成单条普通命令,且前几个参数逐个相同。
  • 路径规则:/dir/** 管整棵子树,/dir/* 只管一层,否则精确匹配;从 matchPathRule 看,规则里的 ~ 不会展开(packages/agent-modules/permission/src/tools/fs-permission.ts:729)。fs 是个伞形工具名,一条 fs 规则同时作用于 read、write、edit、glob、grep、list(fs-permission.ts:92)。
  • 整工具规则:不带内容,如上例的 web_fetch;MCP 工具还可以按服务整体放行或拒绝。

回复怎样落盘(packages/local-runtime/src/api/local-permission-approval-service.ts:140):

选项写入效果
Allow for this conversation会话级 allow本会话再遇到匹配的调用不再问,新会话会再问
Always allow matching actions全局 allow所有会话生效
Deny会话级 deny本会话内同样的调用直接拒绝

写的内容来自判定时给出的 ruleContents。Bash 默认存完整参数列表,比如 cargo build --release 存成 ["cargo","build","--release"],参数一变就会再问(bash-checker.ts:331);edit、write 的放行规则则存在伞形名 fs 下(packages/local-runtime/src/api/host-permission-rules.ts:5)。中止回合时,挂起的询问按拒绝收场,但不写规则(local-permission-approval-service.ts:336)。

与 Plan 模式、沙箱的关系

Plan 模式有自己的守卫,排在权限闸门之前(packages/local-runtime-v2/src/services.ts:973):写文件类工具只允许用 write 或 edit 写规范的计划文件,其他一律拒绝(packages/local-runtime-v2/src/service/plan/tool-guard.ts:47)。这个计划文件的路径还会作为“可信精确写入目标”传给权限判定(packages/local-runtime-v2/src/service/turn-system/agent-host/assembly/local-turn-execution-preparation.ts:268),写它不必询问(fs-permission.ts:956)。Plan 守卫只管写文件类工具,bash 不在其列,照常走权限判定。Plan 模式本身见 Plan 模式与目标。

沙箱与权限各管一段:权限决定这次调用能不能跑、要不要改写,沙箱在内核层限制跑起来的 bash 能碰什么,默认关闭,而且只包 bash。引擎的上下文里留着 sandboxEnabled、autoAllowBashIfSandboxed 两个字段,开启时可以让沙箱里的 bash 跳过整工具的 ask 规则(context.ts:57);但本地运行时构造上下文时没有设置它们,这条豁免在命令行版里不生效。

下一篇:沙箱:sandbox-runtime 的分叉——Anthropic 开源沙箱运行时的分叉,怎样在 macOS 上把 bash 关进 Seatbelt。

本页目录