# 工具权限与安全防护 本文说明 JiuwenSwarm **工具调用权限**(`allow` / `ask` / `deny`)如何生效、与 **workspace 外路径**、**内置安全规则**、**用户审批持久化** 的关系,以及 **CLI `/add-dir`** 会改哪些配置。 配置主文件一般为 `~/.jiuwenswarm/config/config.yaml`;可通过环境变量 `JIUWENSWARM_CONFIG_DIR` 指向其它目录(与 [配置说明](配置信息.md) 一致)。 --- ## 1. 总览 | 能力 | 说明 | | ----------- | ---------------------------------------------------------------------- | | **三级动作** | `allow` 直接执行;`ask` 需用户确认(Web/CLI 等);`deny` 拒绝。 | | **主开关** | `permissions.enabled`;关闭后引擎对工具调用返回 `allow`(仍建议生产环境保持开启)。 | | **策略模式** | `permissions.schema: tiered_policy`(及兼容别名)时启用**分层策略**;否则走旧版工具级匹配。 | | **参与校验的通道** | 仅当 `channel_id` 属于 `web` / `acp` / `cli` 等引擎约定集合时才做工具权限检查;其它通道可跳过。 | 数字分身、群聊等场景下 `ask` 可能被降级为 `deny`,见 [频道](频道.md) 中 **owner_scopes** 相关说明。 --- ## 2. 分层策略(`tiered_policy`)下,一次工具调用如何定级 以下对应 `evaluate_tiered_policy()`(`jiuwenswarm/agentserver/permissions/tiered_policy.py`)的**真实分支顺序**。参数指本次的 `tool_name` 与 `tool_args`(如 bash 的 `command`、读文件的 `path` 等)。 ### 2.1 准备:`permission_mode` 与 `severity` - `permissions.permission_mode`:`normal`(默认)或 `strict`。 - 参数级规则(内置或用户)若写了显式 **`action: allow|ask|deny`**,**直接**使用该动作,**不再**看 `severity`。 - 未写 `action` 时,用 **`severity`** 结合 `permission_mode` 映射为动作: | severity | normal 模式 | strict 模式 | |----------|-------------|-------------| | LOW | allow | allow | | MEDIUM | allow | **ask** | | HIGH | **ask** | **ask** | | CRITICAL | **ask** | **deny** | 未知 `severity` 按 **HIGH** 处理。 ### 2.2 参数级规则如何算「命中」 - 规则必须包含当前 **`tool_name`**,且规则里列出的多个工具须属**同一类**(shell / path / network),否则该条规则被跳过。 - **Shell 类**:用 `pattern` 去匹配 `command` / `cmd` 整串(支持通配、`re:` 正则等)。 - **Path 类**:用 `pattern` 去匹配从 `tool_args` 里抽出的**路径形字符串**(常见键名 + 形似路径的值);`re:` 时路径会先 `\` → `/` 再匹配。 - **Network 类**:当前产品设计为参数规则不匹配,见代码注释。 一次调用可以**命中多条**参数规则,后续用 **strictest** 合并(见 2.4)。 ### 2.3 `evaluate_tiered_policy` 逐步流程(核心) 1. **整工具基线** `_baseline_level`:读 `permissions.tools.`。 - 若为 **`deny`**:**立即返回 DENY**,不再看任何参数规则、override、defaults。 - 若为 `allow` / `ask` / 未配置:先记下来,供后面无参数命中时使用。 2. **收集内置参数规则命中** `builtin_hits`(`builtin_rules.yaml` 经 `get_builtin_security_rules()`)。 - 若其中**任一**命中项为 **DENY**:**立即返回**(只根据内置命中做 `_finalize_hits`,内置 **DENY 优先于同层其它级别**)。 3. **收集用户参数规则命中** `user_hits`(`permissions.rules`)。 - 若其中**任一**命中项为 **DENY**:**立即返回**(用户参数级 **DENY** 在此阶段生效)。 - 注意:此处在 **`approval_overrides` 之前**,因此用户规则里的 **deny** 可以拦住后续本应命中的 override(override 不会被执行到)。 4. **`approval_overrides`**(仅 `action: allow` 的条目参与): - 若**至少一条**对本次 `tool_name` + `tool_args` 匹配成功:**立即返回 ALLOW**,`matched_rule` 带 `tiered_policy:approval_overrides:...` 前缀。 - **不**覆盖第 2 步已返回的内置 DENY(因根本走不到这里)。**不**覆盖第 3 步的用户参数 DENY。 5. **若未因 override 返回**:若 **`builtin_hits` 非空**,则 **`_finalize_hits(builtin_hits)` 并返回**——**不再使用** `user_hits` 参与最终级别合并。 - 含义:只要内置层有**任意**参数级命中(且前面没有 deny、没有 override),**最终结果只反映内置命中**;用户 `rules` 里同一次调用上的其它命中**不会**与内置做「跨层 strictest」。 6. **若 `builtin_hits` 为空** 且 **`user_hits` 非空**:`_finalize_hits(user_hits)` 并返回。 7. **若参数级全无命中**:若整工具基线 **`bl` 已配置**(`allow`/`ask`),返回该级别及 `tools.`。 8. 否则若存在 **`defaults."*"`**,解析为级别并返回。 9. 否则返回 **ASK**,`matched_rule` 为 `tiered_policy:fallback(no_config)`(引擎里可能再当作「无配置」处理)。 `_finalize_hits`:若命中列表里含 **DENY**,结果为 **DENY**;否则对命中项的 `allow/ask` 取 **strictest**(`deny > ask > allow` 严格序)。 ### 2.4 引擎在 `tiered_policy` 之后还多做什么 在 `PermissionEngine.evaluate_global_policy_directly` 中,在拿到 `evaluate_tiered_policy` 的结果后还会: 1. **`maybe_escalate_shell_operators`**:若结果**不是**来自 `approval_overrides`,且工具为 shell 类,**allow** 可能因命令中含链式/注入风险字符被 **升为 ask**。 2. **路径防护(可选)**:若路径层开启(`file_guard` / Legacy `external_directory`),用 `FileGuardChecker` 再判路径;与当前级别做 **strictest** 合并。 `check_permission` 路径上通常先算**不含**外部的 tiered 结果,再单独合并外部维(实现上与「一步算完」等价于对用户可见的合并效果,细节见 `core.py`)。 --- ## 3. 内置安全规则 `builtin_rules.yaml` - **包内默认**:`jiuwenswarm/resources/builtin_rules.yaml`。 - **用户覆盖**:与 `config.yaml` **同目录**下的 `builtin_rules.yaml`(即 `JIUWENSWARM_CONFIG_DIR` 或默认 `~/.jiuwenswarm/config/`),若存在则**优先加载**。 内置规则多为 **shell 高危命令**(删除、格式化、下载执行、提权等),部分条目带显式 `action: deny`。用户 `rules` 不能覆盖内置的 deny(内置 deny 先返回)。 --- ## 4. 路径防护:`file_guard` 与 `external_directory` 路径访问由 **Pipeline B**(agent-core `FileGuardChecker`)判定,与工具级 tiered_policy(Pipeline A)做 **strictest** 合并。 ### 4.1 `file_guard`(推荐) ```yaml permissions: file_guard: enabled: true defaults: {read: ask, write: ask, exec: ask} # 绑定运行时 agent workspace(resolve_workspace_dir),路径不写死在 YAML workspace: read: allow write: allow exec: ask paths: - path: "/data/public" read: allow write: ask exec: deny - path: "**/.env*" match: glob read: ask write: ask exec: ask ``` - **独立开关**:`file_guard.enabled: false` 时整层路径防护关闭(含旧外部目录检查),不影响 `tools` / `rules`。 - **`workspace`**:按 R/W/X 放行**运行时** `workspace_root`(通常为 `~/.jiuwenswarm/agent/workspace`);拿不到 root 时跳过该规则并打 WARN。Native **无** Legacy 式隐式 workspace 放行,须显式配置本项或 `paths`。 - **`trusted_dirs`**:TUI 项目 cwd / `/add-dir` 热更新目录,由引擎编译为前缀 allow(独立于 `workspace`)。 - **R/W/X**:同一路径可分别配置;Write/Exec⇒Read 蕴含,显式 `read: deny` 优先。 - **匹配**:`match: prefix`(缺省,最长前缀)或 `glob`。 - **Native vs Legacy**:显式配置 `defaults` / `workspace`(R/W/X)或 `match: glob` 时进入 Native;仅有 `external_directory` 时走 Legacy 投影(workspace 内隐式放行)。 ### 4.2 `external_directory`(deprecated,只读兼容) 旧配置 `permissions.external_directory`(如 `"*": ask`、具名前缀 `allow`)加载期投影进 FileGuard Legacy,**行为与现网 ExternalDirectory 等价**。 **新写入**(`/add-dir`、HITL「总是允许」路径)请写 `file_guard.paths`,不再写 `external_directory` 具名键。 --- ## 5. `approval_overrides`(用户「总是允许」等持久化) 在审批流程中选择**记住规则**或等价持久化逻辑时,会向 `permissions.approval_overrides` 追加条目,字段通常包括: - `id`、`tools`、`match_type`(`path` / `command`)、`pattern`、`action`(如 `allow`)、`source`(如 `user_approval`、`cli_add_dir`)。 **Shell 类** `pattern` 若以 `re:` 开头,为正则匹配**整条命令字符串**。 **路径信任**优先写入 `file_guard.paths`;`/add-dir` **不再**追加 path 类 override,仍可追加 **command** 类 override 以消除 shell 命令文本中的 ASK。 HITL「总是允许」路径层落盘规则: - 写入**触达路径本身**(`ls …/projects` → `…/projects`),**不上卷父目录** - 按当时 action 设轴:`read` → 仅 `read: allow`;`write` → `read/write: allow`;`exec` → `read/exec: allow`(其余轴保持 `ask`) - **不再**写 path 类 `approval_overrides`(A 线只保留工具 / 命令匹配;路径只认 B) - 若当时 A 为 `tools.write_file: ask`,「总是允许」会抬升为 **`tools.write_file: allow`**(整工具),并另由 B 写入 `file_guard.paths` - `/add-dir` 仍为目录信任:`read/write: allow`,`exec: ask` 默认配置下纯路径写工具(`write` / `write_file` / `edit_file` / `search_replace`)为 **`allow`**,日常路径拦截只走 `file_guard`;若用户把某路径工具改回 `ask`,HITL「总是允许」可再把它抬回整工具 `allow`。`tools.*.ask` **不会**被 B 放行绕过。 --- ## 6. `/permissions` 命令使用指南 TUI 中 `/permissions` 是管理工具权限与规则的快捷入口,支持 **交互式管理** 和 **命令行直设** 两种模式。 ### 6.1 交互式管理(无参数) ``` /permissions ``` 无参数调用时,会先请求 `permissions.tools.get` 和 `permissions.rules.get`,按 **ASK / DENY / ALLOW** 三级分组展示概况: ``` ── ASK ── 工具 8 · 规则 22 ── DENY ── 工具 1 · 规则 0 ── ALLOW ── 工具 33 · 规则 0 ``` 随后弹出选择菜单,依次进入管理: #### ① 选择权限组 ``` [权限管理] 选择要管理的权限组: ASK(30 项) 工具 8 · 规则 22 DENY(1 项) 工具 1 · 规则 0 ALLOW(33 项) 工具 33 · 规则 0 退出 ``` #### ② 查看组内详情 + 管理 进入某个组(如 ASK)后,**工具** 和 **规则** 分开列出: ``` ── 工具(8)── bash mcp_exec_command write_file ── 规则(22)── bash, mcp_exec_command (+1)(cat *) pattern: cat * bash(ls *) pattern: ls * ... --- + 添加 添加工具或参数规则到当前组 返回上级 回到级别选择 ``` 选中某项后可: - **移到 ALLOW / ASK / DENY** — 更改级别 - **删除** — 移除该工具或规则 - **返回** — 返回列表 #### ③ 添加新条目(工具或规则) 选择 `+ 添加` 后,输入一行文本,系统自动解析: ``` [添加到 ASK] 当前组: ASK | 添加工具: 输入工具名 (如 write_file) | 添加规则: 输入 工具名(匹配模式) (如 bash(ls *)) | Esc 取消 → Other ← 先选此项,再输入内容 (Esc 取消) ``` | 输入格式 | 效果 | |----------|------| | `write_file` | 添加工具权限,级别为当前组(ASK) | | `bash(ls *)` | 添加参数规则,`tools: [bash]`, `pattern: ls *`, `action: ask` | | `bash(re:.*rm -rf.*)` | 添加正则参数规则 | > 必须先选中 `Other` 选项进入文本输入模式,再键入内容。按 Enter 提交,Esc 取消。 ### 6.2 命令行直设模式 ``` /permissions # 工具级权限 /permissions () # 参数级规则 ``` | 命令 | 效果 | |------|------| | `/permissions ask write_file` | 写入文件前需用户确认 | | `/permissions allow bash` | 允许 bash 直接执行 | | `/permissions deny bash` | 拒绝所有 bash 调用(最高优先级,会跳过参数规则) | | `/permissions allow bash(ls *)` | 允许 `ls *` 直接执行 | | `/permissions deny bash(re:.*rm -rf.*)` | 拒绝含 `rm -rf` 的命令 | ### 6.3 pattern 写法 - **普通文本**:字面匹配,如 `ls *`(glob 风格通配) - **正则**:以 `re:` 开头,如 `re:.*\.env$`(匹配整条命令字符串) ### 6.4 action vs severity `/permissions` 命令将用户设置的 `allow/ask/deny` **直接写入规则的 `action` 字段**,而非通过 `severity` 间接映射。引擎解析参数级规则时,若存在显式 `action`,则**直接使用该动作,不再查 severity 映射表**(参见 §2.1)。 这意味着: - `/permissions deny bash(re:.*rm -rf.*)` → `action: deny` → **任何模式下都拒绝**(不依赖 `permission_mode`) - `/permissions ask write_file(re:.*\.env$)` → `action: ask` → **任何模式下都需确认** > **为什么不用 severity?** 之前的实现将 `deny` 映射为 `severity: CRITICAL`,但在 `normal` 模式下 CRITICAL 的实际效果是 `ask` 而非 `deny`,违背了用户意图。改为直接写 `action` 后,用户的 `allow/ask/deny` 意图得到忠实表达。 ### 6.5 规则 ID 生成 - **命令行模式**:格式为 `cli_rule__`,例如 `cli_rule_bash_ls__` - **交互式添加**:同上。ID 基于 `tool + pattern` 生成,不依赖 action,因此**同一条规则不会因 action 不同而重复创建**。重复添加会提示"该规则已存在"。 ### 6.6 错误提示 | 情况 | 提示 | |------|------| | 级别不在 allow/ask/deny | `无效级别 "xxx",仅允许:allow、ask、deny` | | 工具名为空 | `工具名不能为空。` | | 工具名含括号但无 pattern | 按工具级权限处理(括号部分被忽略) | | 规则 ID 已存在 | `该规则已存在,请在列表中选择后修改。` | | API 请求失败 | 显示具体 RPC 方法名和错误信息 | ### 6.7 与配置文件的对应关系 | 操作 | 写入的配置路径 | RPC 方法 | |------|-------------|---------| | 工具级权限设置 | `permissions.tools. = "level"` | `permissions.tools.update` | | 删除工具 | 从 `permissions.tools` 移除 | `permissions.tools.delete` | | 创建参数规则 | `permissions.rules` 数组新增条目 | `permissions.rules.create` | | 修改规则 action | `permissions.rules` 中对应条目的 `action` | `permissions.rules.update` | | 删除参数规则 | 从 `permissions.rules` 移除 | `permissions.rules.delete` | | 无参数查看 | 读取 `permissions.tools` + `permissions.rules` | `permissions.tools.get` + `permissions.rules.get` | ### 6.8 后端调用链路 ``` permissions.ts (TUI 前端) ↓ ctx.request(method, params) app-state.ts ↓ wsClient.request(id, method, params, timeout) ws-client.ts ↓ JSON frame via WebSocket Gateway (tui_connect.py) → 白名单校验(ALLOWED_METHODS) → 转发到 AgentServer AgentServer (permissions_config_rpc.py) → dispatch_permissions_config_request() → update_permissions_tool_in_config / create_permissions_rule_in_config / ... config.py → load_yaml_round_trip(config.yaml) → 修改 permissions 段 → dump_yaml_round_trip(config.yaml) ``` 共 **7 个 RPC 接口**: | 方法 | 用途 | |------|------| | `permissions.tools.get` | 获取所有工具权限 | | `permissions.rules.get` | 获取所有参数规则 | | `permissions.tools.update` | 设置/更新工具级别 | | `permissions.tools.delete` | 删除工具权限 | | `permissions.rules.create` | 创建新规则 | | `permissions.rules.update` | 修改规则(如改 action) | | `permissions.rules.delete` | 删除规则 | > **注意**:`/permissions` 设置的是全局工具权限。数字人/群聊场景下的 `owner_scopes` 权限需通过频道面板单独配置,详见 [频道](频道.md)。 --- ## 7. CLI:`/add-dir` 与 `persist_cli_trusted_directory` 终端 TUI 中信任目录(如 `/workspace add` → `command.add_dir`)会调用宿主 `persist_cli_trusted_directory`,行为要点: 1. **`file_guard.paths`**:为解析后的目录写入一条前缀规则:`read: allow`、`write: allow`、**`exec: ask`**(目录信任 ≠ 默认可执行该树下二进制)。 2. **若使用带 overrides 的变体且 `schema` 为 `tiered_policy`**:可追加 **shell 类** `approval_overrides`(`match_type: command`);**不再**写 path 类 override。 3. **Shell 侧 pattern**:仅生成正斜杠路径字面片段,避免 YAML 中 `\U` 非法转义;运行时对命令文本做 `\` → `/` 再匹配。 旧键 `external_directory.: allow` 仍可读(过渡期);新写入只走 `file_guard.paths`。 --- ## 8. 相关文件(开发参考) | 模块 | 路径 | | --------------- | ----------------------------------------------------------------------------------- | | 分层策略 / file_guard | agent-core:`openjiuwen/harness/security/{tiered_policy,file_guard,core}.py` | | 宿主落盘 `/add-dir` | `jiuwenswarm/agents/harness/common/rails/permissions/permissions_persist.py` | | HITL 落盘 overlay | `jiuwenswarm/agents/harness/common/rails/interrupt/interrupt_helpers.py` | | TUI `/permissions` | `jiuwenswarm/channels/tui/frontend/src/core/commands/builtins/permissions.ts` | | WebSocket 命令 | `jiuwenswarm/server/agent_ws_server.py`(`command.add_dir`) | --- ## 9. 另见 - [配置说明](配置信息.md):`JIUWENSWARM_CONFIG_DIR`、配置文件位置。 - [命令行指令](命令行指令.md):CLI/TUI 使用入口(含斜杠命令)。 - [频道](频道.md):`owner_scopes`、数字分身与 `ask` 降级。