# 权限模式与规则

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

- 作者：David（道雾轩）
- 专栏：MiniMax Code 源码解读（https://daiw.net/manual/minimax-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.net/manual/minimax-code/permission
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

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

和 [OpenCode 的权限系统](https://daiw.net/manual/opencode/permission)那种“默认全放行、最后一条匹配说了算”的规则模型相比，这里默认更保守，判定也分了更多层。

## 怎么用

TUI 里有三种模式（`packages/tui/src/application/permission-mode.ts:1`），配置里还多一个 `off`：

| 配置值 | TUI 名称 | 行为 |
| --- | --- | --- |
| `default` | Ask | 判定为询问就弹审批框 |
| `auto` | Auto | 询问先交给云端分类器，它放行就不问；没登录 MiniMax 账号时退化为 Ask |
| `bypassPermissions` | Full 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 判定 | 7593 | `tools/bash-*.ts` 等 15 个文件：命令拆分、包装剥离、写目标、慢命令、首词快速放行、规则匹配 |
| 路径判定 | 2814 | `fs-permission.ts`、`path-capability.ts`、`fs-checker.ts` 等：工作区边界、敏感文件、路径规则 |
| 核心 | 2674 | `engine.ts`、`permission-core.ts`、`context.ts`、`types.ts`、`execution-plan.ts` 等 |
| 危险模式 | 1754 | `classifier/` 目录：HARD、SOFT 两套危险模式正则，外加是否走云端分类的判断 |
| 本地化与宿主接口 | 1161 | 原因文案、语言检测、`configurePermissionHost` 与包入口 |
| 云端网关与询问策略 | 603 | Auto 模式调用的网关客户端、对话摘要、`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`）。

## 一次调用的审批链

```mermaid
flowchart TD
  A["pi 准备执行工具"] --> B["钩子链：插件 PreToolUse、Plan 模式与目标预算、扩展钩子"]
  B --> C["LocalTurnPermissionGate"]
  C --> D["LocalPermissionFacade.checkPermission"]
  D -->|allow| E["套用执行计划后放行"]
  D -->|deny| F["工具错误：Permission denied"]
  D -->|ask| G["审批服务发出 permission.ask"]
  G --> H["TUI 审批框"]
  H -->|允许| E
  H -->|拒绝或中止| F
```

**挂载点**：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`）：

```ts
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` |
| 3 | HARD 最终拒绝类别 | 拒绝 | `:1172` |
| 3.5 | 无界的全盘扫描，如不带 `-maxdepth` 的 `find /` | 拒绝，并提示改用更窄的命令 | `:1192` |
| — | Full access 到此为止 | `rm` 改写为可恢复删除；其他含删除动词的命令拒绝；其余放行 | `:1230` |
| 4 | HARD 敏感读取，如读 `/etc/shadow` | 询问 | `:1297` |
| 5 | 用户 allow 规则 | 放行 | `:1312` |
| 5.5 | 写入目标在工作区等已授权位置 | 放行 | `:1328` |
| 6 | 按子命令分级的危险模式，如 `sudo` | 询问 | `:1341` |
| 6b | 读取环境里的密钥，如 `printenv` 某个凭据 | 询问 | `:1360` |
| 6.5 | `source` 本地临时文件或工作区脚本 | 放行 | `:1390` |
| 7 | SOFT 预扫描 | 询问 | `:1402` |
| 8 | `rm` | 放行并标记为可恢复删除 | `: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 工具](https://daiw.net/manual/minimax-code/file-and-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`）：

```ts
  // 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 文件：

```json
{
  "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 模式与目标](https://daiw.net/manual/minimax-code/plan-and-goal)。

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

下一篇：[沙箱：sandbox-runtime 的分叉](https://daiw.net/manual/minimax-code/sandbox)——Anthropic 开源沙箱运行时的分叉，怎样在 macOS 上把 bash 关进 Seatbelt。
