目录

权限与安全 — Ruleset 驱动的细粒度控制

权限与安全 — Ruleset 驱动的细粒度控制

源码路径:/mnt/e/code/cc/opencode/packages/opencode/src/permission/ 核心文件:permission/index.ts 技术栈:Effect + Pattern 匹配 + Zod


1. 概述

OpenCode 的权限系统采用 Ruleset 数组 模型,相比 Claude Code 的 PermissionMode 枚举更加精细化。每个 Agent 持有一个权限规则数组,通过 pattern 匹配实现对工具、文件路径和命令的细粒度控制。


2. Ruleset 类型

// packages/opencode/src/permission/index.ts
export namespace Permission {
  export const Rule = z.object({
    permission: z.string(),   // 权限类型 (read, edit, bash, task, etc.)
    pattern: z.string(),      // 匹配模式 (glob 风格)
    action: z.enum(["allow", "deny", "ask"]),
  })

  export type Ruleset = z.infer<typeof Rule>[]
}

权限类型

// 文件操作
"read"        // 读取文件
"edit"        // 编辑文件
"write"       // 写入文件
"glob"        // 文件模式匹配
"grep"        // 搜索
"ls"          // 目录列表
"bash"        // Shell 命令

// 网络操作
"webfetch"    // HTTP 请求
"websearch"   // 网络搜索
"codesearch"  // 代码搜索

// 任务管理
"task"        // 派生子 Agent
"todowrite"   // Todo 列表修改
"question"    // 用户交互

// 其他
"external_directory"  // 访问工作区外目录
"lsp"         // LSP 操作
"doom_loop"   // 循环检测豁免

3. Agent 默认权限

3.1 build Agent

{
  permission: [
    // 全部允许
    { permission: "read", pattern: "*", action: "allow" },
    { permission: "edit", pattern: "*", action: "allow" },
    { permission: "bash", pattern: "*", action: "allow" },
    { permission: "glob", pattern: "*", action: "allow" },
    { permission: "grep", pattern: "*", action: "allow" },
    // 外部目录白名单
    { permission: "external_directory", pattern: "~/**", action: "allow" },
    { permission: "external_directory", pattern: "/tmp/**", action: "allow" },
  ]
}

3.2 plan Agent

{
  permission: [
    { permission: "read", pattern: "*", action: "allow" },
    { permission: "glob", pattern: "*", action: "allow" },
    { permission: "grep", pattern: "*", action: "allow" },
    // 仅允许编辑计划文件
    { permission: "edit", pattern: ".opencode/plans/*", action: "allow" },
    // 禁止所有写入和执行
    { permission: "write", pattern: "*", action: "deny" },
    { permission: "bash", pattern: "*", action: "deny" },
  ]
}

3.3 explore Agent

{
  permission: [
    { permission: "read", pattern: "*", action: "allow" },
    { permission: "glob", pattern: "*", action: "allow" },
    { permission: "grep", pattern: "*", action: "allow" },
    // 禁止所有修改操作
    { permission: "edit", pattern: "*", action: "deny" },
    { permission: "bash", pattern: "*", action: "deny" },
    { permission: "write", pattern: "*", action: "deny" },
  ]
}

4. 权限评估

// packages/opencode/src/permission/index.ts
export function evaluate(
  permission: string,   // 权限类型 (如 "bash")
  pattern: string,      // 资源路径 (如 "/path/to/file")
  ruleset: Ruleset,     // 规则数组
): { action: "allow" | "deny" | "ask"; rules: Rule[] } {
  // 1. 找出匹配该权限类型的所有规则
  const matched = ruleset.filter(rule =>
    rule.permission === permission &&
    matchesGlob(pattern, rule.pattern)
  )

  // 2. 优先级: deny > ask > allow
  //    最后一条匹配的规则决定结果
  if (matched.length === 0) {
    return { action: "ask", rules: [] }
  }

  const last = matched[matched.length - 1]!
  return { action: last.action, rules: matched }
}

优先级规则

  • deny 规则总是优先
  • ask 作为中间状态
  • allow 作为默认行为
  • 规则按顺序匹配,最后一条生效

5. 权限请求流程

// 工具执行时请求权限
async function executeTool(tool: Tool.Def, args: unknown, ctx: Tool.Context) {
  const { action } = Permission.evaluate(
    tool.id,
    extractFilePaths(args),  // 从参数中提取文件路径
    ctx.agent.permission,
  )

  switch (action) {
    case "allow":
      return tool.execute(args, ctx)

    case "deny":
      throw new Permission.DeniedError({
        tool: tool.id,
        args,
        rules: matchedRules,
      })

    case "ask": {
      // 暂停执行,等待用户确认
      await ctx.ask({
        permission: tool.id,
        patterns: extractFilePaths(args),
        always: [],  // 用户的 "Always" 选择
        sessionID: ctx.sessionID,
        metadata: { tool: tool.id },
      })

      // 用户确认后重新评估
      const { action: retryAction } = Permission.evaluate(...)
      if (retryAction === "deny") {
        throw new Permission.DeniedError(...)
      }
      return tool.execute(args, ctx)
    }
  }
}

用户响应选项

interface Request {
  permission: string
  patterns: string[]
  always?: string[]  // "Always allow" 选项
}

// 用户响应
type Replied = {
  type: "once"    // 临时允许一次
} | {
  type: "always"  // 永久添加到白名单
} | {
  type: "reject"  // 拒绝
}

6. 配置覆盖

{
  "permission": {
    "read": [
      { "pattern": "*", "action": "allow" },
      { "pattern": "**/secrets/**", "action": "deny" }
    ],
    "bash": [
      { "pattern": "rm -rf /**", "action": "deny" },
      { "pattern": "git **", "action": "allow" },
      { "pattern": "npm **", "action": "allow" }
    ]
  }
}

7. 与 Claude Code 对比

维度Claude CodeOpenCode
权限模型PermissionMode 枚举 (browse/auto/bypass/plan)Ruleset 数组 + Pattern 匹配
文件级别Glob Pattern 精细控制
命令级别Bash 命令白名单
外部目录单一开关白名单 Pattern
Doom loop简单计数权限豁免机制
永久授权“Always” 选项
企业 MDMManaged Config 覆盖
配置位置config.tomlJSONC + config.json

7.1 关键差异

Claude Code 的权限模型是粗粒度的:

  • browse — 只读
  • auto — 自动批准安全操作
  • bypass — 无限制
  • plan — 仅计划模式

OpenCode 的 Ruleset 是细粒度的:

  • 每种权限都可以有独立的 Pattern 规则
  • 可以针对特定文件/目录设置不同行为
  • 支持 deny/ask/allow 三级控制
  • 用户可以选择 “Always” 永久授权

8. 核心文件索引

文件职责
packages/opencode/src/permission/index.tsRuleset 定义 + 评估逻辑

文档版本:v1.0 | 更新:2026-04-06