Cat Blog
HomeBlogTools
Search
Language
Choose site style
Choose accent color
Click Effect
Theme

Cat Blog · Updated regularly. Source code is available on GitHub.

Index 03: Permission — 让智能体做事前先问一声

July 29th, 2026
AI后端

Series

使用Kotlin从0开发一个ClaudeCode技术文章

Series

使用Kotlin从0开发一个ClaudeCode

Progress 3 / 21

使用Kotlin从0开发一个ClaudeCode

Previous in series

Index 02: Tool Use — 让智能体动手做事

Next in series

Index 04: Hooks — 给工具链装上前后扩展点

目标

Index 02 的智能体会用工具了——读文件、写文件、执行 Shell 命令。但它也有了一个危险的能力:LLM 说 bash rm -rf /,AgentLoop 就会执行。

Index 03 的目标是给智能体装上安全阀,在工具执行前插入一道审批管线:

  1. 定义 PermissionRule 接口和 PermissionResult 三态枚举,让每条规则都能对工具调用做出 ALLOW / DENY / ASK 的判断
  2. 实现 PermissionPipeline 责任链模式,串联多个规则,收集所有结果再汇总——DENY 始终覆盖 ASK
  3. 实现三个内置规则——ToolCategoryRule(读写分类)、DangerousCommandRule(危险命令检测)、PathAllowlistRule(路径逃逸防护)
  4. 实现 ApprovalStore 记忆存储,用户的 "always allow" 决定不会在每次工具调用时重复询问
  5. 实现 ReadLinePrompter 终端交互,用简洁的交互界面让用户确认/拒绝/记住
  6. 通过回调注入 AgentLoop,不修改 AgentLoop 对 permission 包的依赖关系,保持零耦合

完成后的效果——用户重新掌握了控制权:

TEXT
┌─ Permission Required ─────────────────────────────
│ Tool:   bash
│ Input:  {"command":"rm -rf /"}
│ Rule:   dangerous-command
├────────────────────────────────────────────────────
│ [y] Yes — allow this time
│ [n] No  — deny this time
│ [a] Always allow this tool
│ [d] Always deny this tool
└────────────────────────────────────────────────────
>

读文件自动放行,rm -rf / 直接拒绝,写文件需要用户点头。安全、灵活、可记忆。


架构:可插拔的审批管线

Index 03 在 Index 02 的工具执行链路上插入了一个可选的审批步骤:

TEXT
                            AgentLoop 工具执行循环
                                   │
    ┌──────────────────────────────▼──────────────────────┐
    │  executeOneTool(toolCall)                            │
    │                                                      │
    │  ① onBeforeToolExecute? ───null─────→ 直接执行      │
    │       │ (回调)                                       │
    │       ▼                                              │
    │  ② PermissionPipeline.approve(toolCall)              │
    │       │                                              │
    │       ├──③ 收集所有规则的 evaluate() 结果            │
    │       │       │                                      │
    │       │       ├─ToolCategoryRule  → ALLOW / ASK      │
    │       │       ├─DangerousCommand → ALLOW / DENY      │
    │       │       └─PathAllowlistRule → ALLOW / DENY     │
    │       │                                              │
    │       ├──④ 汇总结果                                  │
    │       │   ├─任一 DENY        → 返回 false (拒绝)     │
    │       │   ├─全部 ALLOW       → 返回 true (放行)      │
    │       │   └─存在 ASK (无DENY) → 进入交互确认         │
    │       │           │                                  │
    │       │           ├─ApprovalStore 有记忆 → 直接决定  │
    │       │           └─ReadLinePrompter     → 用户选择  │
    │       ▼                                              │
    │  ⑤ 返回:true=执行 / false=Permission denied 错误    │
    └──────────────────────────────────────────────────────┘
层Index 02 状态Index 03 变更行数变化
权限模型无PermissionResult, PermissionRule, PermissionDecision, UserPrompter+133 行
审批管线无PermissionPipeline(收集-汇总-交互)+104 行
记忆系统无ApprovalStore(ConcurrentHashMap)+51 行
终端交互无ReadLinePrompter(y/n/a/d 提示)+61 行
三个规则无ToolCategoryRule + DangerousCommandRule + PathAllowlistRule+150 行
智能体AgentLoop 工具循环回调注入(12 行代码, 0 耦合)203→219 行
REPL注册工具 + 启动循环权限管线组装 + 注入 AgentLoop+20 行

第一层:权限数据模型

PermissionResult:三态枚举

PermissionRule 对工具调用的评估结果是三个值之一:

KOTLIN
enum class PermissionResult {
    ALLOW,  // 此规则认为可以执行
    DENY,   // 此规则认为不应执行(覆盖 ASK)
    ASK,    // 需要用户确认
}

DENY 和 ASK 的区别是关键设计决策。DENY 是无条件的——无论其他规则怎么说,只要有一个规则返回 DENY,管线直接拒绝。ASK 则给用户留了选择空间,但如果另一个规则已经返回了 DENY,用户不会被打扰。

PermissionRule:单方法接口

KOTLIN
interface PermissionRule {
    val name: String
    suspend fun evaluate(toolCall: ToolCall): PermissionResult
}

极简接口,只有一个方法。name 用于日志和用户提示("Rule: dangerous-command")。evaluate() 是 suspend 函数——虽然当前实现全是同步的,但接口以协程设计为后续预留了空间(比如需要查远程 ACL 的规则)。

PermissionDecision:用户的四种选择

KOTLIN
enum class PermissionDecision {
    ALLOW,         // 仅本次允许
    DENY,          // 仅本次拒绝
    ALLOW_ALWAYS,  // 始终允许此工具
    DENY_ALWAYS    // 始终拒绝此工具
}

在交互环节,用户的选择比规则的评估结果多了两种——"记住"选项。ALLOW_ALWAYS 和 DENY_ALWAYS 会被写入 ApprovalStore,后续对同一工具名的调用直接跳过用户提示。

UserPrompter:交互接口

KOTLIN
interface UserPrompter {
    suspend fun ask(toolCall: ToolCall, ruleName: String): PermissionDecision
}

与 PermissionRule 分离的独立接口。管线只依赖接口,不关心实现——REPL 下是 ReadLinePrompter(stdin/stdout),测试下是 FakePrompter(预设决策序列)。这个分离让 PermissionPipelineTest 可以在纯内存中运行,不需要终端。


第二层:PermissionPipeline — 收集-汇总-交互

权审批管线的核心逻辑分四步:

KOTLIN
class PermissionPipeline(
    private val rules: List<PermissionRule>,
    private val approvalStore: ApprovalStore,
    private val userPrompter: UserPrompter
) {
    suspend fun approve(toolCall: ToolCall): Boolean {
        // 1. 收集所有规则的评估结果
        val results = rules.map { rule ->
            try { rule.evaluate(toolCall) }
            catch (e: Exception) { PermissionResult.DENY } // 异常→DENY
        }

        // 2. 汇总:任一 DENY → 拒绝
        if (results.any { it == PermissionResult.DENY }) return false

        // 3. 全部 ALLOW → 放行
        val hasAsk = results.any { it == PermissionResult.ASK }
        if (!hasAsk) return true

        // 4. 存在 ASK → 查记忆 → 交互
        val remembered = approvalStore.get(toolCall.name)
        if (remembered != null) return remembered == PermissionResult.ALLOW

        val decision = userPrompter.ask(toolCall, askRuleName)
        // 处理 ALLOW_ALWAYS / DENY_ALWAYS → 写入记忆
        ...
    }
}

收集→汇总→交互 三步走的设计有几个隐蔽的优点:

异常容错。 rules.map 中的 try/catch 确保任何一个规则抛出异常不会导致整个审批崩溃——异常被视为 DENY,安全地拒绝工具调用。这防止了规则代码的 bug 变成安全漏洞。

DENY 优先级不受注册顺序影响。 无论 DangerousCommandRule 在规则列表的开头还是末尾,只要它返回 DENY,管线就拒绝。这避免了"过早 ALLOW"的问题——如果一个靠前的规则返回了 ALLOW,后面的 DENY 仍然有效。

ApprovalStore 位于 UserPrompter 之前。 记忆中已有的决策不会触发终端交互,即使在用户输入的 /clear 清空记忆之前不需要重复审批。


第三层:三个内置规则

ToolCategoryRule — 读写自动分类

KOTLIN
class ToolCategoryRule : PermissionRule {
    override val name = "tool-category"

    override suspend fun evaluate(toolCall: ToolCall): PermissionResult {
        val isReadOnly = try {
            ToolRegistry.get(toolCall.name).isReadOnly
        } catch (_: UnknownToolException) {
            return PermissionResult.ASK  // 未知工具→安全默认
        }
        return if (isReadOnly) PermissionResult.ALLOW
        else PermissionResult.ASK
    }
}

利用 Tool 接口的 isReadOnly 属性做自动分类。读操作(read_file)自动放行,写操作(write_file、bash)标记为"需要确认"。未知工具也视为 ASK——如果 LLM 编造了一个不存在的工具名,用户会被提示。

这是管线中典型的 "base rule"——它不应该单独决定执行或拒绝,而是为后续规则提供初始信号。如果放在规则列表首位,后续的 DangerousCommandRule 可以在 ASK 已经标记的情况下进一步检查并返回 DENY。

DangerousCommandRule — 七种致命模式

KOTLIN
class DangerousCommandRule : PermissionRule {
    override val name = "dangerous-command"

    override suspend fun evaluate(toolCall: ToolCall): PermissionResult {
        if (toolCall.name != "bash") return PermissionResult.ALLOW

        val command = toolCall.input["command"]?.toString() ?: ""

        val isDangerous = DANGEROUS_PATTERNS.any { pattern ->
            pattern.containsMatchIn(command)
        }
        return if (isDangerous) PermissionResult.DENY else PermissionResult.ALLOW
    }

    companion object {
        private val DANGEROUS_PATTERNS = listOf(
            Regex("rm\\s+-rf\\s+/"),           // 递归强制删除根目录
            Regex("chmod\\s+777\\s+/"),        // 修改根目录权限
            Regex("dd\\s+if="),                // 磁盘直接写入
            Regex("mkfs\\."),                  // 格式化文件系统
            Regex(">\\s*/dev/sd[a-z]"),        // 覆盖磁盘设备
            Regex(":.*\\{\\s*:\\|:&\\s*\\};:"), // fork 炸弹
            Regex("sudo\\s+rm\\s+-rf\\s+/")    // sudo 删除根目录
        )
    }
}

仅对 bash 工具生效,用七个正则表达式匹配已知危险模式。匹配结果直接返回 DENY——不给用户选择,因为普通用户也不应该为 "Do you want to run rm -rf /?" 这种问题做选择。

正则列表是硬编码的,不是可配置的。设计哲学:Deny by default for dangerous, Ask by default for unknown。不追求完美覆盖所有危险命令——漏掉的模式(false negative)仍然会被 ToolCategoryRule 标记为 ASK,让用户有机会判断;误杀(false positive)也可以通过用户选择 "Allow" 绕过。

PathAllowlistRule — 防止路径逃逸

KOTLIN
class PathAllowlistRule : PermissionRule {
    override val name = "path-allowlist"
    private val projectRoot = Path.of(System.getProperty("user.dir")).normalize()

    override suspend fun evaluate(toolCall: ToolCall): PermissionResult {
        if (toolCall.name !in APPLICABLE_TOOLS) return PermissionResult.ALLOW

        val pathString = extractPath(toolCall) ?: return PermissionResult.ALLOW

        return try {
            val resolved = projectRoot.resolve(pathString).normalize()
            if (resolved.startsWith(projectRoot)) PermissionResult.ALLOW
            else PermissionResult.DENY
        } catch (_: Exception) { PermissionResult.ALLOW }
    }

    companion object {
        private val APPLICABLE_TOOLS = setOf("write_file", "bash")
    }
}

对 write_file 和 bash 工具生效,检查操作的目标路径是否在项目根目录内。write_file("../outside/file.txt") 和 write_file("/etc/passwd") 都会被识别为路径逃逸并返回 DENY。

核心逻辑一句话:resolved.startsWith(projectRoot)。路径先 resolve 再 normalize,../ 和符号链接都会被正确解析。

保守策略:宁可漏过也不误杀。当无法提取路径时返回 ALLOW,当路径解析异常时返回 ALLOW。因为这个规则之后还有 ToolCategoryRule 的 ASK 兜底——漏过的写入行为仍然会触发用户确认。


第四层:ApprovalStore — 记住用户的决定

KOTLIN
class ApprovalStore {
    private val decisions = ConcurrentHashMap<String, PermissionResult>()

    fun remember(toolName: String, decision: PermissionResult) { ... }
    fun get(toolName: String): PermissionResult? { ... }
    fun clear() { ... }
}

用 ConcurrentHashMap 按工具名存储用户的 "always allow" / "always deny" 决定。选择 ConcurrentHashMap 而非 HashMap 的原因是 AgentLoop 支持并发工具执行——虽然权限检查发生在并发执行之前,但 ApprovalStore 是跨线程共享的状态,需要线程安全。

clear() 方法在 REPL 的 /clear 命令中被调用,清空对话历史的同时也清空权限记忆,给用户一个"重置"的选项。

当前是纯内存存储(重启后丢失),持久化留给 Index 12 Task System。


第五层:ReadLinePrompter — 终端交互

KOTLIN
class ReadLinePrompter : UserPrompter {
    override suspend fun ask(toolCall: ToolCall, ruleName: String): PermissionDecision {
        println("┌─ Permission Required ─────────────────────────────")
        println("│ Tool:   ${toolCall.name}")
        println("│ Input:  ${formatInput(toolCall)}")
        println("│ Rule:   $ruleName")
        println("├────────────────────────────────────────────────────")
        println("│ [y] Yes — allow this time")
        println("│ [n] No  — deny this time")
        println("│ [a] Always allow this tool")
        println("│ [d] Always deny this tool")
        println("└────────────────────────────────────────────────────")
        // 读取用户输入,直到 y/n/a/d 之一
    }

    private fun formatInput(toolCall: ToolCall): String {
        val raw = toolCall.input.toString()
        return if (raw.length > 120) raw.take(120) + "..." else raw
    }
}

用终端 UI 展示待审批的工具调用详情。formatInput() 截断超过 120 字符的 JSON 输入,防止超长参数(比如 write_file 的整个文件内容)占满终端。

输入处理:大小写不敏感,Ctrl+D(EOF)视为拒绝。循环直到收到有效输入,提示信息明确显示 y/n/a/d 四个选项。

交互方式的选择:四个按键覆盖所有场景(允许/拒绝/始终允许/始终拒绝),不需要额外输入。后续可以考虑扩展为更丰富的交互("先问再记"、"仅本次记住"),但 Index 03 的设计以简洁优先。


第六层:回调注入 — 松耦合集成

这是 Index 03 最克制的设计决策。AgentLoop 不 import permission 包的任何内容:

KOTLIN
class AgentLoop(
    private val llmProvider: LLMProvider,
    private val systemPrompt: String,
    private val config: AgentConfig = AgentConfig(),
    private val onBeforeToolExecute: (suspend (ToolCall) -> Boolean)? = null
)

onBeforeToolExecute 是一个可空的函数类型参数。null(默认值)意味着"不做权限检查",保持 Index 01-02 的所有现有测试零改动。

在执行工具时,如果回调不为 null 且返回 false,工具被跳过并返回 "Permission denied: xxx" 错误消息:

KOTLIN
private suspend fun executeOneTool(toolCall: ToolCall): ToolResult {
    if (onBeforeToolExecute != null) {
        val approved = onBeforeToolExecute.invoke(toolCall)
        if (!approved) {
            return ToolResult(toolCall.id, "Permission denied: ${toolCall.name}", isError = true)
        }
    }
    // ... 执行工具
}

Permission denied 被包装为 isError=true 的 ToolResult,作为 tool_result 回传给 LLM。LLM 收到这个错误后会调整行为——可能换一个安全的工具重试,或者告诉用户 "这个操作被拒绝了"。

依赖方向: AgentLoop → ?(callback) 不依赖 permission 包。ReplLoop 负责组装 PermissionPipeline 并注入为 lambda。如果将来要移除权限系统,只需在 ReplLoop 中传 null,不需要修改 AgentLoop 一行代码。

在 ReplLoop 中的组装代码:

KOTLIN
class ReplLoop(...) {
    fun start() {
        // 注册工具
        ToolRegistry.register(ReadFileTool())
        ToolRegistry.register(WriteFileTool())
        ToolRegistry.register(BashTool())

        // 组装权限管线
        approvalStore = ApprovalStore()
        val pipeline = PermissionPipeline(
            rules = listOf(
                ToolCategoryRule(),
                DangerousCommandRule(),
                PathAllowlistRule()
            ),
            approvalStore = approvalStore,
            userPrompter = ReadLinePrompter()
        )

        // 注入 AgentLoop
        agentLoop = AgentLoop(
            llmProvider = llmProvider,
            systemPrompt = systemPrompt,
            config = config,
            onBeforeToolExecute = { tc -> pipeline.approve(tc) }
        )
        // ...
    }
}

踩坑记录

1. PermissionPipelineTest 中 askRuleName 的获取

在 Pipeline 的实现中,当需要交互时,需要找出"哪个规则触发了 ASK",以便在用户提示中显示规则名。最初的设计是在 userPrompter.ask() 中传入 askRuleName 字符串。

实现时的挑战:如何在不重复执行 evaluate() 的情况下获取触发 ASK 的规则名。解决方法是重新遍历 rules,找到第一个返回 ASK 的规则:

KOTLIN
val askRuleName = rules.firstOrNull { r ->
    try { r.evaluate(toolCall) == PermissionResult.ASK }
    catch (_: Exception) { false }
}?.name ?: "unknown"

这段代码不可避免地需要重新执行 evaluate(),但这是可以接受的——规则评估本身是轻量级的(纯内存逻辑),不需要 I/O。如果未来有重量级规则(如远程 ACL 查询),可以考虑在第一步 collect 时同时存储规则名。

2. ConcurrentHashMap 的选择

ApprovalStore 初始实现使用了普通的 HashMap,但在重构中发现 AgentLoop 的并发工具执行会导致跨线程访问问题。虽然权限检查在 executeOneTool() 中是同步的(coroutineScope 内的 async 仍然在同一协程内执行),但从设计上 ApprovalStore 应该支持并发访问,避免未来引入 bug。

3. PathAllowlistRule 对 bash 工具的启发式路径提取

对 bash 工具,PathAllowlistRule 的 extractPath() 方法当前返回 null——因为从 shell 命令中可靠地提取文件路径是不现实的(命令可能是 cat ../outside/db.sqlite、cp /etc/passwd .、find / -name "*.key")。null 意味着跳过路径检查(返回 ALLOW),依赖 ToolCategoryRule 的 ASK 兜底。

这不是一个完美的解决方案——逃逸路径的 bash 命令不会自动拒绝,而是落入"用户确认"的环节。但这是有意为之:路径逃逸的用户提示给用户提供了判断机会,而自动拒绝可能阻塞合法操作(比如在项目内执行 cat /usr/share/dict/words,虽然不在项目内但是读操作,应该放行)。

4. PermissionPipelineTest 的 FakePrompter 设计

FakePrompter 的行为是"超出序列时重复最后一个决策"——与 Index 02 的 FakeLLMProvider 策略一致。在 PermissionPipelineTest 中,ASK 场景的测试只需要一个决策(用户选择 Allow 或 Deny),但"记忆"场景的测试中 FakePrompter 根本不会被调用。FakePrompter 的"溢出重复"设计让测试代码更简洁——不必为不会被调用的交互预设多余的决策。

5. ToolCategoryRule 对 ToolRegistry 的运行时依赖

ToolCategoryRule 依赖 ToolRegistry 来查询工具的 isReadOnly 属性。这意味着规则不能在工具注册之前执行——如果 AgentLoop 在 ReplLoop.start() 完成注册之前尝试审批,会抛出 UnknownToolException。事实上不会发生(AgentLoop 在 ReplLoop.register() 之后才创建),但如果有人从 AgentLoopTest 直接创建 AgentLoop 并注入 callback,需要在测试中记得注册工具。


测试:33 个新用例,99 总计

Index 03 新增 33 个测试,总用例数从 66 增长到 99。测试策略延续 Index 02 的 Fake 模式:

PermissionPipelineTest(11 个用例): 使用 FakeRule(假规则)和 FakePrompter(预设决策序列的假 UserPrompter),覆盖全部决策组合:

  • 全部 ALLOW → 放行
  • 任一 DENY → 拒绝(2 个:单独 DENY、DENY 覆盖 ASK)
  • ASK + 记忆 ALLOW/DENY → 直接决定(2 个)
  • ASK + 用户选择 Yes/No → 交互决定(2 个)
  • ASK + 用户选择始终允许/始终拒绝 → 记忆存储(2 个)
  • 规则抛异常 → 视为 DENY
  • 空规则列表 → 默认放行
  • ThrowingRule 和 FakeRule 的组合测试异常容错

三个规则的独立测试(14 个用例):

  • ToolCategoryRule(3 个):只读→ALLOW、写入→ASK、未注册→ASK
  • DangerousCommandRule(6 个):safe→ALLOW、rm -rf→DENY、sudo rm→DENY、dd→DENY、非 bash→ALLOW、npm test→ALLOW
  • PathAllowlistRule(5 个):内部路径→ALLOW、../跳出→DENY、/etc/passwd→DENY、bash 无路径→ALLOW、非适用工具→ALLOW

ApprovalStore(5 个用例): 记忆→读取、覆盖、未记住返回 null、clear 清空、多次 remember 覆盖

AgentLoop 集成测试(3 个用例): 回调返回 true/false/null 三种场景

被测组件测试方式Index 02Index 03合计
EnvFile临时文件8—8
ConfigResolverKotest10—10
MessageKotest5—5
AnthropicProviderMockEngine10—10
AgentLoopFakeLLMProvider12+315
ToolRegistryFakeEchoTool5—5
ReadFileTool临时文件5—5
WriteFileTool临时目录5—5
BashToolProcessBuilder5—5
PermissionPipelineFakeRule + FakePrompter—1111
ApprovalStoreKotest—55
ToolCategoryRuleFakeTool + ToolRegistry—33
DangerousCommandRuleToolCall 构造—66
PathAllowlistRule临时目录计算—55
总计65+33+1

EnvFileTest 从 Index 02 的 8 个增长到 9 个(由于前一阶段的一个小修复),所以实际索引 02 的基线是 66 个测试。新增 33 个后总计 99 个。


文件清单

TEXT
src/main/kotlin/com/sepcai/code/
├── permission/                          # [新建] 权限包
│   ├── PermissionResult.kt              # ALLOW/DENY/ASK 三态枚举
│   ├── PermissionRule.kt                # 权限规则接口
│   ├── UserPrompter.kt                  # 用户交互接口 + PermissionDecision 枚举
│   ├── ApprovalStore.kt                 # 审批记忆存储(ConcurrentHashMap)
│   ├── PermissionPipeline.kt            # 审批管线(收集-汇总-交互)
│   ├── ReadLinePrompter.kt              # 终端交互实现(y/n/a/d)
│   └── rules/
│       ├── ToolCategoryRule.kt          # 读写自动分类规则
│       ├── DangerousCommandRule.kt      # 危险命令检测规则(7 种模式)
│       └── PathAllowlistRule.kt         # 路径逃逸防护规则
├── agent/
│   ├── AgentLoop.kt                     # [修改] +onBeforeToolExecute 回调, 203→219 行
└── repl/
    └── ReplLoop.kt                      # [修改] +权限管线组装注入, 欢迎语 Index 03

src/test/kotlin/com/sepcai/code/
├── agent/
│   └── AgentLoopTest.kt                 # [修改] +3个权限集成测试, 12→15 个测试
├── permission/                          # [新建] 权限测试包
│   ├── PermissionPipelineTest.kt        # 11 tests (FakeRule + FakePrompter)
│   ├── ApprovalStoreTest.kt             # 5 tests
│   └── rules/
│       ├── ToolCategoryRuleTest.kt      # 3 tests
│       ├── DangerousCommandRuleTest.kt  # 6 tests
│       └── PathAllowlistRuleTest.kt     # 5 tests

16 个源文件(9 新建 + 2 修改)· 5 个新测试文件 + 1 个测试修改 · 99 个测试 · 全部通过。


下一站

Index 03 的权限管线已经就位,但审批和工具执行之间还缺少一个关键环节——非功能性扩展点。用户可能想在每次工具执行前记录日志、测量性能,或在执行后发送通知。

Index 04: Hooks 将引入 HookManager,支持 PreToolUse 和 PostToolUse 两个扩展点。核心设计:

  • HookEvent 统一事件模型:携带事件类型、工具调用、执行结果等上下文
  • HookHandler 函数类型:接收 HookEvent,返回 HookResult(允许修改 ToolResult)
  • HookManager 事件分发:fire 事件 → 收集处理结果 → 继续或中断工具执行

Permission(Index 03)和 Hooks(Index 04)是一对互补的扩展点——Permission 做安全决策(放行/拒绝),Hooks 做非功能性增强(日志/指标/通知)。两者共同构成了工具执行链路的前后扩展能力。

Table of Contents

Current section:目标

  • 1. 目标
  • 2. 架构:可插拔的审批管线
  • 3. 第一层:权限数据模型
  • 4. PermissionResult:三态枚举
  • 5. PermissionRule:单方法接口
  • 6. PermissionDecision:用户的四种选择
  • 7. UserPrompter:交互接口
  • 8. 第二层:PermissionPipeline — 收集-汇总-交互
  • 9. 第三层:三个内置规则
  • 10. ToolCategoryRule — 读写自动分类
  • 11. DangerousCommandRule — 七种致命模式
  • 12. PathAllowlistRule — 防止路径逃逸
  • 13. 第四层:ApprovalStore — 记住用户的决定
  • 14. 第五层:ReadLinePrompter — 终端交互
  • 15. 第六层:回调注入 — 松耦合集成
  • 16. 踩坑记录
  • 17. 1. PermissionPipelineTest 中 askRuleName 的获取
  • 18. 2. ConcurrentHashMap 的选择
  • 19. 3. PathAllowlistRule 对 bash 工具的启发式路径提取
  • 20. 4. PermissionPipelineTest 的 FakePrompter 设计
  • 21. 5. ToolCategoryRule 对 ToolRegistry 的运行时依赖
  • 22. 测试:33 个新用例,99 总计
  • 23. 文件清单
  • 24. 下一站
Back to top

Related Posts

View all posts
Index 21: Terminal UX —— Claude Code 风格终端交互

Index 21: Terminal UX —— Claude Code 风格终端交互

August 21st, 2026

Index 21 将 cat-code 终端交互升级为 Claude Code 风格,解决旧 REPL 黑箱、无中断及输入体验差的问题。通过 JLine3 与 Mordant 实现历史补全、实时工具可见性、Spinner 状态及 Esc 中断。核心采用 UI 与 AgentLoop 解耦的事件流架构,支持内联权限菜单与优雅降级。同时配置 logback 收敛控制台日志,确保 TUI 清爽且功能无损,显著提升可用性。

Index 20: Comprehensive Agent —— 全机制集成(收口)

Index 20: Comprehensive Agent —— 全机制集成(收口)

August 13th, 2026

作为收官之作,把 s01-s19 的二十个核心机制整合为一个全面智能体:统一的状态查询与运行周期、自治认领与工作区隔离的贯通,以及 /status 命令的全局可视化。全机制协同运转,标志着 Cat-Code 从零完整复刻 Claude Code 核心能力的收官。

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

August 13th, 2026

引入 MCP(Model Context Protocol)插件机制,通过多传输适配与通道路由,把外部 MCP 服务器的工具动态接入智能体的工具池。工具注册从静态编译期扩展为运行时动态组装,让智能体能力随外部服务即插即用。