AstroResearch/docs/architecture/agent/hooks.md
Asfmq 698d007f39 feat: Agent 安全纵深防御、Checkpoint 快照、会话 Rewind/Branch、自进化
Skill、流式执行优化与系统架构全面升级

  本次提交对标 Claude Code 与 Hermes-Agent 的工程细节,在安全、可靠性、
  会话管理、自我进化四个维度进行了系统性加固,变更总量 48 文件 / +12680 -2292 行。

  ═══════ 安全纵深防御 ═══════

  1. Hardline 硬阻止层 (src/agent/runtime/hardline.rs, +534 行)
     - 不可绕过的危险命令拦截(关重启、磁盘擦除、Fork 炸弹、rm -rf /、kill -1)
     - 反规避标准化管线: ANSI 序列剥离 → Unicode NFKC → shell 反斜杠还原 → 空字面量清理
     - 在 PermissionChecker 之前执行,YOLO/Bypass 模式下同样生效
     - 集成到 executor Phase 2,被拒绝工具直接注入错误结果

  2. Permission 优先级裁决器 (src/agent/runtime/permission.rs, +200 行)
     - 7 层正式优先级规则 (P0 Deny → P7 Allow),带冲突日志
     - explain() 方法支持审计追溯
     - Hook PermissionRequired 与 Checker 结果的正确叠加逻辑

  ═══════ Checkpoint 文件快照系统 ═══════

  3. git2 原生快照 (src/agent/runtime/checkpoint.rs, +920 行)
     - 基于 git2 bare repo,内容寻址自动去重
     - 文件变更操作前自动触发 (file_write/file_edit/run_bash)
     - 每目录每 turn 最多一次快照,防止同一轮重复
     - 支持 list/diff/restore API + pre-rollback 安全快照
     - 旧快照自动 prune(保留最近 N 个)+ 按目录隔离 ref
     - 排除规则自动过滤 node_modules/target/.git/*.pdf 等
     - 集成到 executor: 文件操作前 ckpt.ensure_checkpoint()

  ═══════ 错误恢复系统大升级 ═══════

  4. 21 种 FailoverReason 分类 (src/agent/runtime/error_recovery.rs, +1200 行)
     - 参考 Hermes-Agent error_classifier.py
     - 8 步分类管线: provider-specific → HTTP status → text pattern → error body → fallback
     - is_retryable / should_compress / should_failover / is_permanent 方法
     - Context Overflow 自动修复: 从错误消息提取 token 限制,自动下调预算
     - RecoveryStep::AdjustMaxTokens 实现 (参考 Claude Code 自动修复)
     - 向后兼容 ErrorKind 别名

  ═══════ 会话 Rewind / Branch / Retry 体系 ═══════

  5. 完整 undo 栈 (src/agent/runtime/session.rs, +800 行 + 2 迁移脚本)
     - Rewind (软删除): active=0 标记,审计 trail 保留,LLM 不可见
     - Restore (撤销回退): 冲突检测——回退后有新消息则拒绝,引导使用 Branch
     - Branch: 分叉会话,复制所有 active=1 消息到新会话
     - Retry: 硬删除最后一轮对话,返回原消息文本供前端重提交
     - 数据库: agent_messages.active 列 + agent_sessions.rewind_count + parent_session_id
     - API: 4 个新端点 (/branch, /retry, /rewind, /rewind/restore)
     - load_history_for_agent 全面使用 active=1 过滤

  ═══════ Hooks 系统模块化重构 ═══════

  6. 单文件 → 7 模块体系 (src/agent/hooks/)
     hooks.rs (994 行) 拆分为:
     - mod.rs    — 入口 + HookRegistry + SessionHookManager
     - types.rs  — 类型定义 (Context, TaggedContext, PermissionRequestAction 等)
     - traits.rs — AgentHook + AsyncAgentHook + 15 种生命周期事件
     - matcher.rs — 工具名/参数匹配 + session 作用域过滤
     - dispatch.rs — 并行调度引擎 (run_pre/post_tool_use 等)
     - registry.rs — 注册/注销/查询
     - builtins.rs — CancellationHook + MetricsHook + AuditLogHook + ContextDeduplicator

     关键改进:
     - run_pre_tool_use 并行执行所有匹配 hooks,聚合 Block/MutateInput/Continue
     - TaggedContext 带完整来源标记的上下文注入 (hook_name + event)
     - ContextDeduplicator 单 dispatch cycle 内内容哈希去重
     - AsyncAgentHook 支持 fire-and-forget 异步 hooks

  ═══════ Executor 并发执行升级 ═══════

  7. 三阶段管道重写 (src/agent/runtime/executor.rs, +600 行)
     - Phase 1: 死循环检测 + 参数解析 (不变)
     - Phase 2: Hardline 预检查 (新增) → PermissionChecker (改进)
     - Phase 3: ToolPartitioner 分区 → 逐批次执行 (重写)
       - 并行批次内 FuturesUnordered 并发
       - 串行批次确保非并发安全工具独占执行
       - Checkpoint 预触发集成
     - Hook 上下文注入: system-reminder 格式 + ContextDeduplicator 去重
     - Hook 阻塞错误详细记录

  ═══════ 流式执行真正的流式调度 ═══════

  8. StreamingExecutor 重写 (src/agent/runtime/streaming_executor.rs, ~400 行变更)
     - on_tool_use 中对并发安全工具立即 tokio::spawn,不等待 flush
     - executing_non_concurrent 标志阻塞后继工具直到独占工具完成
     - JoinHandle 管理替代自定义 cancel channel
     - completed_queue 按流顺序 yield
     - Sibling Abort 通过 broadcast channel + tokio::select! 竞速
     - ToolContext 实现 Clone (支持 per-task 上下文复制)

  ═══════ 自改进 Skill 系统 ═══════

  9. PatternDetector + SkillCreator + Curator (src/agent/skills/, +1500 行)
     - PatternDetector: 扫描 agent_messages 表,检测跨 session 重复工具调用模式
     - SkillCreator: 将高置信度模式自动生成 SKILL.md (YAML frontmatter + 工作流步骤)
     - SelfImprovePipeline: 一站式 模式检测 → 创建 → 质量审查
     - Curator: 分析 skill 使用统计,标记 stale/deprecated,建议清理
     - Skill frontmatter 新增 pinned 字段 (禁止 Curator 自动清理)

  ═══════ 基础设施优化 ═══════

  10. 系统提示词缓存 (src/agent/runtime/system_prompt.rs + mod.rs)
      - SystemPromptCache: 首次计算后永久复用,/clear 时失效
      - 新增 SAFETY / SYSTEM_CONTEXT / TOOL_USAGE 静态 section
      - 环境/tools/skills/memory 动态 section 通过 get_or_compute 缓存

  11. ToolRegistry schema 缓存 (src/agent/tools/mod.rs)
      - schema_cache + schema_generation 版本号
      - 工具变更/过滤器变更时自动失效
      - precompute_definitions() 预计算 (AgentRuntime 初始化时调用)

  12. 迭代摘要融合 (src/agent/compact.rs, +100 行)
      - 参考 Hermes context_compressor.py
      - CollapseLog 追踪压缩历史,支持溢出合并
      - extract_prior_summary: 提取已有摘要融入新压缩

  13. SubAgent 系统提示词模块化 (src/agent/tools/subagent.rs)
      - 复用 5 个标准 section + 子代理专有上下文 section
      - 独立 ToolRegistry 构建工具列表

  ═══════ 前端 — CSS 变量主题系统 ═══════

  14. 全新主题变量体系 (dashboard/src/index.css + App.tsx + 各面板)
      - CSS 自定义属性: --bg-card, --text-main, --text-muted, --border-precision
      - 语义化颜色: --accent-blueprint, --accent-star
      - 全面替换硬编码 Tailwind 颜色 (slate-xxx → var(--xxx))
      - 文献入库提示优化 ("核心知识节点" 替代 "向量块")
      - ReaderPanel 样式变量化
2026-06-22 20:29:37 +08:00

922 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Agent Hooks — 生命周期事件系统
参考 Claude Code hooks 协议,提供 **12 种生命周期事件回调**,基于 **观察者模式 + 责任链模式** 实现。核心目标:在 Agent ReAct 循环的各个关键节点插入横切关注点(取消检查、指标采集、审计日志、权限增强等),**不污染主循环代码**。
---
## 源码结构
```
src/agent/hooks/
├── mod.rs — 模块入口(声明 + 重导出 + 测试,~540 行)
├── types.rs — 数据类型定义(事件、上下文、动作、结果,~370 行)
├── traits.rs — AgentHook + AsyncAgentHook trait~120 行)
├── matcher.rs — ToolNamePattern / ToolMatchFilter 工具匹配器(~300 行)
├── registry.rs — HookRegistry 结构体 + 基础方法(~280 行)
├── dispatch.rs — HookRegistry 调度方法(所有 run_*~700 行)
└── builtins.rs — 内置 HooksCancellationHook、MetricsHook、AuditLogHook、ContextDeduplicator~260 行)
```
---
## 设计哲学
```mermaid
mindmap
root((Hook 系统<br/>设计原则))
接口隔离
10 个生命周期方法
+2 个权限专用方法 (on_permission_request/on_permission_denied)
全部有默认空实现
只覆写关心的 hook 点
事件索引
subscribed_events 声明
预计算 event_index
避免无关 hook 调用
工具级过滤
match_filter 声明
ToolNamePattern 匹配
content_patterns 过滤
并行优先
无依赖事件 join_all
有副作用事件顺序执行
OnSessionStop 例外
超时隔离
per-hook timeout 可配置
默认 30 秒
超时跳过不拖垮全部
聚合不丢
additional_contexts 全保留
tagged_contexts 有来源
blocking_errors 全收集
warnings/metadata 不丢失
双模式共存
全局 hooks 构造时注册
session hooks 运行时动态
AsyncAgentHook 异步 fire-and-forget
析构自动清理
```
---
## 架构总览
```mermaid
graph TB
subgraph RT["AgentRuntime::run_turn()"]
direction TB
Start["OnSessionStart"] --> Loop["ReAct Loop"]
Loop --> Comp["PreCompact / PostCompact"]
Loop --> Stop["OnSessionStop"]
end
subgraph Registry["HookRegistry"]
direction LR
Index["event_index<br/>HashMap&lt;HookEvent, Vec&lt;usize&gt;&gt;<br/>预计算索引"]
AIndex["async_event_index<br/>异步 hook 索引"]
SH["session_hooks<br/>HashMap&lt;String, Vec&lt;Box&lt;dyn AgentHook&gt;&gt;<br/>运行时动态管理"]
end
subgraph Executor["executor::execute_parallel()"]
direction LR
Pre["PreToolUse<br/>并行 join_all + timeout + match_filter"]
Exec["工具执行"]
Post["PostToolUse<br/>并行 join_all + timeout + match_filter"]
Fail["PostToolUseFailure<br/>is_error 时触发"]
Async["AsyncAgentHook<br/>fire-and-forget, 5s 超时"]
end
subgraph Builtins["内置 Hooks3 个 sync + 0 async"]
CH["CancellationHook<br/>订阅: PreToolUse, OnSessionStop"]
MH["MetricsHook<br/>订阅: OnSessionStart, PostToolUse,<br/>OnStepComplete, OnSessionStop"]
AH["AuditLogHook<br/>订阅: PostToolUse, OnSessionStop"]
end
RT --> Registry
Loop --> Executor
Registry --> Builtins
Executor --> Registry
Executor --> Async
```
---
## 类型系统
```mermaid
classDiagram
class AgentHook {
<<trait>>
+name() &str
+subscribed_events() &[HookEvent]
+timeout() Option~Duration~
+match_filter() ToolMatchFilter
+on_session_start(ctx)
+pre_tool_use(ctx) PreToolUseAction
+post_tool_use(ctx) PostToolUseAction
+on_post_tool_use_failure(ctx) PostToolUseAction
+on_step_complete(ctx)
+on_session_stop(ctx)
+on_subagent_start(ctx)
+on_subagent_stop(ctx)
+on_pre_compact(ctx)
+on_post_compact(ctx)
+on_permission_request(ctx) PermissionRequestAction
+on_permission_denied(ctx)
}
class AsyncAgentHook {
<<trait>>
+name() &str
+subscribed_events() &[HookEvent]
+on_post_tool_use_async(ctx)
+on_post_tool_use_failure_async(ctx)
+on_session_stop_async(ctx)
}
class HookEvent {
<<enumeration>>
OnSessionStart
PreToolUse
PostToolUse
PostToolUseFailure
OnStepComplete
OnSessionStop
OnSubagentStart
OnSubagentStop
OnPreCompact
OnPostCompact
PermissionRequest
PermissionDenied
}
class ToolMatchFilter {
+name_patterns: Vec~ToolNamePattern~
+content_patterns: Vec~String~
+matches(tool_name, tool_args) bool
+exact(name) Self
+prefix(name) Self
+is_match_all() bool
}
class ToolNamePattern {
<<enumeration>>
Exact(String)
Prefix(String)
Wildcard
+parse(pattern) Self
+matches(tool_name) bool
}
class PreToolUseAction {
<<enumeration>>
Continue
Block ~reason: String~
MutateInput ~updated_args, additional_context~
PermissionRequired ~permission, tool_name~
}
class PermissionRequestAction {
<<enumeration>>
Continue
Override ~decision: PermissionDecision, reason~
InjectContext ~context: String~
}
class PermissionDecision {
<<enumeration>>
Allow
Ask
Deny
}
class PermissionDenialSource {
<<enumeration>>
Rule
Classifier
User
Timeout
}
class PostToolUseAction {
<<enumeration>>
Continue
MutateOutput ~updated_content, additional_context~
Warning ~message, truncate_output~
PermissionRequired ~permission, tool_name~
Metadata ~key, value~
}
class PreToolUseResult {
+action: PreToolUseAction
+additional_contexts: Vec~String~
+additional_context: Option~String~
+blocking_errors: Vec~BlockingError~
+final_args: Value
+tagged_contexts: Vec~TaggedContext~
}
class PostToolUseResult {
+final_content: String
+additional_contexts: Vec~String~
+tagged_contexts: Vec~TaggedContext~
+warnings: Vec~String~
+post_permission_requests: Vec~(String,String)~
+metadata: HashMap~String, Value~
}
class BlockingError {
+hook_name: String
+reason: String
}
class TaggedContext {
+hook_name: String
+source_event: HookEvent
+content: String
+timestamp: DateTime~Utc~
}
class HookRegistry {
-hooks: Vec~Box~dyn AgentHook~~
-event_index: HashMap
-session_hooks: HashMap
-async_hooks: Vec~Box~dyn AsyncAgentHook~~
-async_event_index: HashMap
+with_builtins(db, cancelled, metrics) Self
+add(hook)
+add_async(hook)
+add_session_hook(sid, hook)
+remove_session_hook(sid, name) bool
+clear_session_hooks(sid)
+run_pre_tool_use(ctx) PreToolUseResult
+run_post_tool_use(ctx) PostToolUseResult
+run_on_session_start(ctx)
+run_on_step_complete(ctx)
+run_on_session_stop(ctx)
+run_on_subagent_start(ctx)
+run_on_subagent_stop(ctx)
+run_on_pre_compact(ctx)
+run_on_post_compact(ctx)
+run_on_post_tool_use_failure(ctx) PostToolUseResult
+run_on_permission_request(ctx) PermissionRequestAction
+run_on_permission_denied(ctx)
}
AgentHook --> HookEvent : subscribes to
AgentHook --> ToolMatchFilter : declares
AgentHook --> PreToolUseAction : returns
AgentHook --> PostToolUseAction : returns
AsyncAgentHook --> HookEvent : subscribes to
HookRegistry --> AgentHook : manages
HookRegistry --> AsyncAgentHook : manages
HookRegistry --> PreToolUseResult : aggregates
HookRegistry --> PostToolUseResult : aggregates
PreToolUseResult --> PreToolUseAction
PreToolUseResult --> BlockingError
PreToolUseResult --> TaggedContext
PostToolUseResult --> TaggedContext
```
### 上下文类型Context Objects
每个生命周期事件携带专用的不可变上下文:
| 事件 | 上下文类型 | 关键字段 |
|------|-----------|---------|
| OnSessionStart | `SessionStartContext` | session_id, turn_index, is_resume |
| PreToolUse | `PreToolUseContext` | session_id, tool_name, tool_args, step |
| PostToolUse | `PostToolUseContext` | session_id, agent_name, tool_name, tool_args, output_content, is_error, step, elapsed_ms |
| PostToolUseFailure | `PostToolUseFailureContext` | session_id, agent_name, tool_name, error_message, is_interrupt, step, elapsed_ms |
| OnStepComplete | `StepCompleteContext` | session_id, step, max_steps, messages_count, estimated_tokens, token_limit |
| OnSessionStop | `SessionStopContext` | session_id, terminal (TurnTerminal), total_steps |
| OnSubagentStart | `SubagentStartContext` | parent_session_id, subagent_name, prompt |
| OnSubagentStop | `SubagentStopContext` | parent_session_id, subagent_name, result_summary, steps, is_error |
| OnPreCompact | `PreCompactContext` | session_id, message_count, estimated_tokens |
| OnPostCompact | `PostCompactContext` | session_id, new_message_count, compression_method |
| PermissionRequest | `PermissionRequestContext` | session_id, tool_name, current_decision, permission_mode, is_subagent |
| PermissionDenied | `PermissionDeniedContext` | session_id, tool_name, reason, source (Rule/Classifier/User/Timeout) |
---
## ToolMatchFilter — 工具级过滤
比 Claude Code 的 `if` 条件更结构化。Hook 通过 `match_filter()` 返回过滤器HookRegistry 在 dispatch 时仅对匹配的工具名和参数调用该 hook。
### ToolNamePattern
| 模式 | 示例输入 | 匹配 |
|------|---------|------|
| `Exact("search_papers")` | `"search_papers"` | ✅ |
| `Exact("search_papers")` | `"download_paper"` | ❌ |
| `Prefix("file_")` | `"file_write"` | ✅ |
| `Prefix("file_")` | `"search_papers"` | ❌ |
| `Wildcard` | 任意 | ✅ |
### ToolMatchFilter
```rust
// 仅匹配文件相关工具且参数路径包含 .env
let filter = ToolMatchFilter {
name_patterns: vec![ToolNamePattern::parse("file_*")],
content_patterns: vec!["path:.env".to_string()],
};
```
- **快捷构造**`ToolMatchFilter::exact("search_papers")`、`ToolMatchFilter::prefix("file_")`
- **默认行为**`ToolMatchFilter::default()` 匹配所有工具
- **仅对** `PreToolUse`、`PostToolUse`、`PostToolUseFailure` 生效
---
## PostToolUseAction — 5 种变体
| 变体 | 用途 | 对执行的影响 |
|------|------|------------|
| `Continue` | 默认,保持输出不变 | 无 |
| `MutateOutput { updated_content, additional_context }` | 修改工具输出内容 | 最终输出替换为 updated_content |
| `Warning { message, truncate_output }` | 标记警告(不阻止) | 注入 `<system-reminder>` |
| `PermissionRequired { permission, tool_name }` | 事后权限请求advisory | 记录审计日志 |
| `Metadata { key, value }` | 附加结构化元数据 | 保留在 PostToolUseResult.metadata 中 |
---
## 异步 Hook (AsyncAgentHook)
`AgentHook` 分离设计,适合后台操作(远程上报、大文件处理等):
```rust
#[async_trait]
pub trait AsyncAgentHook: Send + Sync {
fn name(&self) -> &str;
fn subscribed_events(&self) -> &[HookEvent] { &[] }
async fn on_post_tool_use_async(&self, _ctx: PostToolUseContext) {}
async fn on_post_tool_use_failure_async(&self, _ctx: PostToolUseFailureContext) {}
async fn on_session_stop_async(&self, _ctx: SessionStopContext<'_>) {}
}
```
- **注册**`registry.add_async(Box::new(my_async_hook))`
- **调度**:与 sync hook 并行执行,默认 5s 超时
- **返回值**无——fire-and-forget 语义
---
## TaggedContext — Hook 来源追踪
注入 LLM 的 system-reminder 携带 hook 来源标记:
```
[Hook: AuditLogHook | PostToolUse] 内容...
[Hook: CancellationHook | PreToolUse] 内容...
```
替代原来的匿名 `[Hook 注入上下文]`
---
## ContextDeduplicator — 内容去重
单 dispatch cycle 内防止多个 hook 注入相同上下文消息:
```rust
let mut dedup = ContextDeduplicator::new();
for ctx in &result.hook_contexts {
if !dedup.is_duplicate(ctx) {
messages.push(ChatMessage::user(ctx));
}
}
```
---
## 权限决策优先级
当多个权限来源冲突时,`resolve_permission_precedence()` 按以下优先级裁决:
| Priority | Source | Overridable By |
|----------|--------|----------------|
| **P0** | `PermissionChecker::Deny` | 不可覆盖 |
| **P1** | Tool-level `PermissionRule::Deny` | 不可覆盖 |
| **P2** | Session-level Checker::Deny | 不可覆盖 |
| **P3** | Hook `PreToolUseAction::Block` | P0-P2 |
| **P4** | Hook `PermissionRequired` | P0-P3 |
| **P5** | Session-level Checker::Ask | 仅 Allow → Ask 升级 |
| **P6** | Tool-level `PermissionRule::Ask` | 仅 Allow → Ask 升级 |
| **P7** | `PermissionChecker::Allow` | — |
| **P8** | Implicit Allow (default) | — |
详见 [permission.md](./permission.md)。
---
## PermissionRequest / PermissionDenied — 权限决策钩子
这两个专用事件在权限决策管线中提供 Hook 级别的可编程控制点,参考 Claude Code 的 `PermissionRequest` / `PermissionDenied` 事件设计。
### PermissionRequest — 权限请求前
**触发时机**`resolve_permission_precedence()` 之后、最终权限决策生效之前。
**调度策略**:并行调用所有匹配的 hook**第一个返回 `Override` 的生效**(后续 hook 仍执行但结果忽略),其他 hook 返回 `Continue` 表示不干预。
```mermaid
flowchart LR
subgraph Request["run_on_permission_request()"]
direction LR
H1["Hook 1"] -->|"Override(Allow)"| Win["✅ 首个 Override 生效"]
H2["Hook 2"] -->|"Continue"| Ignore["被忽略"]
H3["Hook 3"] -->|"InjectContext"| Accumulate["累积上下文"]
end
```
**返回值语义**
| 动作 | 效果 |
|------|------|
| `PermissionRequestAction::Continue` | 不干预,沿用 `resolve_permission_precedence()` 的结果 |
| `PermissionRequestAction::Override { decision, reason }` | 覆盖决策:`Allow` 强制放行、`Ask` 升级确认、`Deny` 强制拒绝 |
| `PermissionRequestAction::InjectContext { context }` | 向 LLM 注入额外上下文(如安全策略解释) |
**上下文字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| `session_id` | `String` | 当前会话 ID |
| `tool_name` | `String` | 被请求的工具名 |
| `tool_args` | `Value` | 工具参数(可检查路径、命令等) |
| `current_decision` | `PermissionDecision` | `resolve_permission_precedence()` 的裁决结果 |
| `permission_mode` | `String` | 当前权限模式default/acceptEdits/bypassPermissions/plan |
| `is_subagent` | `bool` | 是否在子代理上下文中 |
### PermissionDenied — 权限拒绝后(审计)
**触发时机**:权限决策结果为 `Deny`(无论来源)。
**调度策略**fire-and-forget 并行调用,不阻塞主循环,返回值忽略。设计用于审计日志、安全告警、拒绝统计。
**来源标记PermissionDenialSource**
| 来源 | 说明 |
|------|------|
| `Rule` | 被 `PermissionChecker` 或工具级规则拒绝 |
| `Classifier` | 被 Auto-mode Classifier 拒绝(待实现) |
| `User` | 用户在交互弹窗中手动拒绝 |
| `Timeout` | 权限请求超时自动拒绝 |
**使用示例**
```rust
struct DenyRateLimiter {
deny_counts: Arc<Mutex<HashMap<String, u32>>>,
}
#[async_trait]
impl AgentHook for DenyRateLimiter {
fn name(&self) -> &str { "DenyRateLimiter" }
fn subscribed_events(&self) -> &[HookEvent] {
&[HookEvent::PermissionRequest, HookEvent::PermissionDenied]
}
async fn on_permission_request(
&self,
ctx: &PermissionRequestContext,
) -> PermissionRequestAction {
let count = self.get_deny_count(&ctx.tool_name);
if count >= 3 {
// 连续拒绝 3 次后强制降级为 Ask不允许自动 Deny
return PermissionRequestAction::Override {
decision: PermissionDecision::Ask,
reason: format!("工具 {} 已被拒绝 {} 次,需要用户确认", ctx.tool_name, count),
};
}
PermissionRequestAction::Continue
}
async fn on_permission_denied(&self, ctx: &PermissionDeniedContext) {
// 记录拒绝统计
*self.deny_counts.lock().unwrap()
.entry(ctx.tool_name.clone()).or_default() += 1;
// 触发安全告警
eprintln!("[SECURITY] 工具 {} 被拒绝: {:?} (来源: {:?})",
ctx.tool_name, ctx.reason, ctx.source);
}
}
```
---
---
## 生命周期事件全景
```mermaid
sequenceDiagram
participant API as API Handler
participant RT as AgentRuntime
participant HR as HookRegistry
participant EX as Executor
participant SA as SubAgentRunner
participant CMP as Compact
Note over API,CMP: ═══ Phase 1: 会话启动 ═══
API->>RT: run_turn(question)
RT->>HR: with_builtins(db, cancelled, metrics)
RT->>HR: run_on_session_start(ctx) ⚡并行
Note over HR: ① OnSessionStart
Note over API,CMP: ═══ Phase 2: ReAct 循环 ═══
loop 每步 (1..max_steps)
RT->>RT: LLM 流式调用 → tool_calls
RT->>EX: execute_parallel(calls, registry)
par 每个工具调用
EX->>HR: run_pre_tool_use(ctx) ⚡并行 + match_filter
Note over HR: ② PreToolUse
HR-->>EX: PreToolUseResult {final_args, tagged_contexts, blocks}
EX->>EX: 权限检查 (Checker + 工具级 + 会话级 + Hook)
EX->>EX: resolve_permission_precedence()
EX->>HR: run_on_permission_request(ctx) ⚡并行 (首个 Override 生效)
Note over HR: ⑪ PermissionRequest — Hook 可覆盖权限决策
alt 权限被拒绝
EX->>HR: run_on_permission_denied(ctx) ⚡并行 (fire-and-forget 审计)
Note over HR: ⑫ PermissionDenied
end
EX->>EX: 工具执行
EX->>HR: run_post_tool_use(ctx) ⚡并行 + match_filter
Note over HR: ③ PostToolUse + AsyncHook dispatch
HR-->>EX: PostToolUseResult {final_content, tagged_contexts, warnings, metadata}
alt is_error
EX->>HR: run_on_post_tool_use_failure(ctx) ⚡并行
Note over HR: ④ PostToolUseFailure + AsyncHook dispatch
end
end
EX-->>RT: ToolExecutionResult {messages, hook_contexts, blocking_errors}
RT->>RT: "hook_contexts → [system-reminder] 注入 (去重)"
RT->>HR: run_on_step_complete(ctx) ⚡并行
Note over HR: ⑤ OnStepComplete
opt 上下文超限
RT->>CMP: compress_context_with_hooks()
CMP->>HR: run_on_pre_compact(ctx) ⚡并行
Note over HR: ⑨ OnPreCompact
CMP->>CMP: compress_with_fallback()
CMP->>HR: run_on_post_compact(ctx) ⚡并行
Note over HR: ⑩ OnPostCompact
end
opt 子代理调用
RT->>SA: SubAgentRunner::run()
SA->>HR: run_on_subagent_start(ctx) ⚡并行
Note over HR: ⑦ OnSubagentStart
SA->>SA: 独立 ReAct 循环
Note over SA: PreToolUse / PostToolUse 同样触发
SA->>HR: run_on_subagent_stop(ctx) ⚡并行
Note over HR: ⑧ OnSubagentStop
SA-->>RT: ToolOutput (summary)
end
end
Note over API,CMP: ═══ Phase 3: 会话收尾 ═══
RT->>RT: finalize_turn()
RT->>HR: run_on_session_stop(ctx) 🔒顺序
Note over HR: ⑥ OnSessionStop
RT-->>API: SSE Done
```
**执行模式图例**:⚡ 并行 `join_all` | 🔒 顺序执行 | ⚡+⏱ 并行 + per-hook timeout
---
## HookRegistry 调度机制
### 事件索引 + 工具过滤
```mermaid
flowchart LR
subgraph Register["add(hook)"]
H["hook: Box&lt;dyn AgentHook&gt;"] --> Subs{"subscribed_events()"}
Subs -->|"空 = 全部"| All["遍历所有 12 个 HookEvent<br/>event_index[event].push(idx)"]
Subs -->|"指定"| Spec["仅注册声明的事件<br/>event_index[event].push(idx)"]
end
subgraph Dispatch["run_*_tool_use(ctx, session_id)"]
Collect["collect_tool_hooks_for(event, sid, tool_name, tool_args)"]
Collect --> GI["event_index[event] → 全局 hooks"]
Collect --> FI["match_filter().matches(tool_name, tool_args) → 过滤"]
Collect --> SI["session_hooks[sid] → session hooks (不过滤)"]
GI --> Merge["统一 Vec&lt;&amp;dyn AgentHook&gt;"]
FI --> Merge
SI --> Merge
end
Register --> Index["event_index: HashMap&lt;HookEvent, Vec&lt;usize&gt;&gt;"]
Dispatch --> Index
```
**关键**`collect_tool_hooks_for()` 在 `collect_hooks_for()` 基础上增加 `match_filter()` 过滤层。全局 hooks 按事件索引 + 工具匹配双重过滤session hooks 不过滤(保持全量响应)。
---
## 数据流Hook 结果如何影响 Agent 行为
```mermaid
flowchart TD
subgraph Executor["executor::execute_parallel()"]
PreHooks["PreToolUse hooks → PreToolUseResult"]
Perms["权限检查 4 层叠加<br/>Checker → 工具级规则 → 会话级规则 → resolve_permission_precedence"]
PermHooks["PermissionRequest hooks → PermissionRequestAction<br/>首个 Override 生效,可覆盖 Allow→Ask 升级或 Deny→Ask 降级"]
PermDenied["PermissionDenied hooks → fire-and-forget 审计<br/>Rule/Classifier/User/Timeout 四种来源标记"]
Tools["工具执行"]
PostHooks["PostToolUse hooks → PostToolUseResult"]
FailHooks["PostToolUseFailure hooks<br/>(仅 is_error"]
AsyncHooks["AsyncAgentHook dispatch<br/>fire-and-forget, 5s 超时)"]
end
subgraph ReactLoop["AgentRuntime::run_react_loop()"]
Push["tool_messages → messages"]
Inject["hook_contexts 包装为 [system-reminder]<br/>(带 TaggedContext 来源 + ContextDeduplicator 去重)"]
LogBlock["blocking_errors → warn! 日志"]
LogWarn["warnings → warn! 日志"]
LogPerm["post_permission_requests → info! 审计"]
StepHook["run_on_step_complete"]
end
PreHooks -->|"final_args 覆盖参数"| Perms
PreHooks -->|"PermissionRequired → Allow→Ask 升级"| Perms
Perms -->|"权限决策"| PermHooks
PermHooks -->|"Override 可覆盖 Allow/Deny/Ask"| Perms
Perms -->|"Deny: 触发"| PermDenied
PermDenied -->|"审计日志"| LogPerm
Perms -->|"Deny: 注入错误 result"| Push
Perms -->|"Allow/Ask/Approve: 进入执行"| Tools
Tools -->|"output"| PostHooks
PostHooks -->|"final_content 覆盖输出"| Push
PostHooks -->|"warnings, metadata, tagged_contexts"| Inject
Tools -->|"is_error"| FailHooks
FailHooks -->|"final_content 覆盖错误输出"| Push
PostHooks -.->|"并行 fire-and-forget"| AsyncHooks
FailHooks -.->|"并行 fire-and-forget"| AsyncHooks
Push --> Inject
Inject --> StepHook
LogBlock --> StepHook
LogWarn --> StepHook
LogPerm --> StepHook
```
### hook_contexts 注入格式
```rust
// 带来源标记TaggedContext
// runtime/mod.rs
let mut dedup = ContextDeduplicator::new();
for ctx in &exec_result.hook_contexts {
if dedup.is_duplicate(ctx) {
continue;
}
let reminder = format!(
"<system-reminder>\n[Hook 注入上下文]\n{}\n</system-reminder>",
ctx
);
messages.push(ChatMessage::user(&reminder));
}
```
内容格式:
```
[Hook: AuditLogHook | PostToolUse] 内容...
[Hook: CancellationHook | PreToolUse] 内容...
```
---
## 内置 Hooks 详解
### CancellationHook
```mermaid
stateDiagram-v2
[*] --> Active: 会话开始
Active --> CheckPreTool: 每次 PreToolUse
CheckPreTool --> Blocked: cancelled_runs 含 session_id
CheckPreTool --> Continue: cancelled_runs 不含 session_id
Blocked --> [*]: Block{reason: "用户已手动中止"}
Continue --> Active: 继续执行
Active --> Cleanup: OnSessionStop
Cleanup --> [*]: cancelled_runs.remove(session_id)
```
依赖 `Arc<Mutex<HashSet<String>>>`(与 `AppState.cancelled_runs` 共享引用)。
### MetricsHook
通过 `from_arc()` 复用 `AgentRuntime` 自身的 `metrics_data: Arc<Mutex<MetricsData>>`,确保 hook 内部采集的数据与 `AgentRuntime::get_metrics()` API 返回的是同一份数据。
订阅事件:`OnSessionStart`(设置 session_id、`PostToolUse`(累加计数)、`OnStepComplete`(每 3 步输出摘要)、`OnSessionStop`(输出终止原因)。
### AuditLogHook
```mermaid
sequenceDiagram
participant EX as Executor
participant AH as AuditLogHook
participant DB as SQLite
EX->>AH: post_tool_use(ctx)
AH->>AH: 构造 status = "OK" | "FAIL"
AH->>AH: output_preview = content[..200]
AH--)DB: "tokio::spawn INSERT agent_audit_log"
EX->>AH: on_session_stop(ctx)
AH--)DB: "tokio::spawn INSERT SESSION_STOP"
```
Fire-and-forget 写入,不阻塞主循环。未来可改为实现 `AsyncAgentHook`
---
## 扩展指南
### 自定义全局 Sync Hook
```rust
struct MyCustomHook;
#[async_trait]
impl AgentHook for MyCustomHook {
fn name(&self) -> &str { "MyCustomHook" }
// 声明只关心 PreToolUse 和 PostToolUseFailure
fn subscribed_events(&self) -> &[HookEvent] {
&[HookEvent::PreToolUse, HookEvent::PostToolUseFailure]
}
// 仅对文件工具生效
fn match_filter(&self) -> ToolMatchFilter {
ToolMatchFilter::prefix("file_")
}
fn timeout(&self) -> Option<Duration> {
Some(Duration::from_secs(10))
}
async fn pre_tool_use(&self, ctx: &PreToolUseContext) -> PreToolUseAction {
if ctx.tool_name == "run_bash" {
return PreToolUseAction::PermissionRequired {
permission: "执行系统命令需要二次确认".into(),
tool_name: ctx.tool_name.clone(),
};
}
PreToolUseAction::Continue
}
async fn post_tool_use(&self, ctx: &PostToolUseContext) -> PostToolUseAction {
if ctx.is_error {
PostToolUseAction::Warning {
message: format!("工具 {} 执行失败", ctx.tool_name),
truncate_output: false,
}
} else {
PostToolUseAction::Continue
}
}
async fn on_post_tool_use_failure(
&self,
ctx: &PostToolUseFailureContext,
) -> PostToolUseAction {
if ctx.is_interrupt {
return PostToolUseAction::Continue;
}
PostToolUseAction::MutateOutput {
updated_content: format!(
"[已由 MyCustomHook 处理] 工具 {} 执行失败: {}。建议尝试替代方案。",
ctx.tool_name, ctx.error_message
),
additional_context: None,
}
}
}
// 注册
registry.add(Box::new(MyCustomHook));
```
### 自定义 Async Hook
```rust
struct RemoteLogger;
#[async_trait]
impl AsyncAgentHook for RemoteLogger {
fn name(&self) -> &str { "RemoteLogger" }
async fn on_post_tool_use_async(&self, ctx: PostToolUseContext) {
// 异步上报工具调用日志到远程服务
tokio::spawn(async move {
upload_to_remote(ctx).await;
});
}
}
registry.add_async(Box::new(RemoteLogger));
```
### 动态 Session Hook
```rust
// 为当前会话临时添加
registry.add_session_hook(&session_id, Box::new(MyTempHook));
// 移除
registry.remove_session_hook(&session_id, "MyTempHook");
// 会话结束时自动清理HookRegistry 析构)
```
---
## 集成点地图
| 文件 | 集成点 | 方法 |
|------|--------|------|
| `runtime/mod.rs` | 会话启动 | `run_on_session_start()` |
| `runtime/mod.rs` | ReAct 循环 | `run_on_step_complete()` |
| `runtime/mod.rs` | Hook 上下文注入 | `hook_contexts → [system-reminder]` |
| `runtime/mod.rs` | 会话终止 | `run_on_session_stop()` |
| `executor.rs` | 工具执行前 | `run_pre_tool_use()` |
| `executor.rs` | 工具执行后 | `run_post_tool_use()` |
| `executor.rs` | 工具执行失败 | `run_on_post_tool_use_failure()` |
| `executor.rs` | 权限决策前 | `run_on_permission_request()` |
| `executor.rs` | 权限拒绝后 | `run_on_permission_denied()` (fire-and-forget 审计) |
| `executor.rs` | 权限决策 | `resolve_permission_precedence()` |
| `subagent.rs` | 子代理启动 | `run_on_subagent_start()` |
| `subagent.rs` | 子代理停止 | `run_on_subagent_stop()` |
| `compact.rs` | 压缩前后 | `run_on_pre_compact()` / `run_on_post_compact()` |
---
## 配置
| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `HOOK_TIMEOUT_SECS` | 30 | 全局 per-hook 超时(秒)。单个 hook 可通过 `timeout()` trait 方法覆盖 |
---
## 设计权衡
| 考量点 | 说明 |
|--------|------|
| **工具级过滤** | `match_filter()` 提供 name + content 双重过滤session hooks 不过滤 |
| **并行 vs 顺序** | 单 hook 自动走顺序路径(无 timeout 开销),多 hook 并行 + timeout |
| **聚合策略** | 最后覆盖 final_args/content全部保留 contexts/blocks/warnings/metadata |
| **OnSessionStop** | 顺序执行(关键清理),`&self` 限制需调用方手动 clear session hooks |
| **AsyncAgentHook** | 独立 traitfire-and-forget 语义5s dispatch 超时,与 sync hook 并行 |
| **TaggedContext** | 带 hook 名称 + 事件来源,格式化注入为 `[Hook: 名称 | 事件]` |
| **ContextDeduplicator** | 内容哈希去重,单 cycle 内有效,不跨 step 去重 |
| **Block 非全局中断** | `PreToolUseAction::Block` 只阻止当前工具的单个调用,全局中断由 `CancellationHook` 实现 |
| **Warning/Post-hoc Permission** | PostToolUse 的 Warning 和 PermissionRequired 均为 advisory不阻塞主循环 |
| **PermissionRequest 调度** | 并行调用所有 hook首个 `Override` 生效,后续 Continue 被忽略;`InjectContext` 全部累积 |
| **PermissionDenied 调度** | fire-and-forget 并行调用,不阻塞主循环,返回值忽略(纯审计用途) |
---
## 变更记录
| 日期 | 变更 |
|------|------|
| 2026-06-22 | Phase 1: 新增 ToolMatchFilter / TaggedContext / 工具级过滤 |
| 2026-06-22 | Phase 2: 新增 resolve_permission_precedence + PermissionChecker::explain |
| 2026-06-22 | Phase 3: PostToolUseAction 扩展 (Warning/PermissionRequired/Metadata) + ContextDeduplicator |
| 2026-06-22 | Phase 4: AsyncAgentHook trait + async hook 调度 |
| 2026-06-22 | 模块拆分: mod.rs(537) ← types/traits/matcher/registry/dispatch/builtins |
| 2026-06-22 | Phase 5: PermissionRequest/PermissionDenied 事件 — 权限决策钩子 + fire-and-forget 审计 |
---
## 测试覆盖
| 测试 | 覆盖路径 |
|------|---------|
| `test_hook_registry_runs_all_hooks` | 注册表遍历调用 |
| `test_blocking_hook_stops_chain` | Block 收集到 blocking_errors |
| `test_mutate_input_accumulates_context` | 参数修改 + 上下文累积 |
| `test_post_tool_use_mutate_output` | 输出修改(含 additional_context |
| `test_cancellation_hook_blocks_when_cancelled` | CancellationHook Block 路径 |
| `test_cancellation_hook_allows_when_not_cancelled` | CancellationHook 放行路径 |
| `test_metrics_hook_accumulates_counts` | MetricsHook 累加正确性 |
| `test_session_start_hook_called` | OnSessionStart 触发 |
| `test_new_lifecycle_events_called` | OnSubagentStart/Stop, Pre/PostCompact 触发 |
| `test_match_filter_skips_irrelevant` | ToolMatchFilter 正确过滤无关工具 |
| `test_full_hook_pipeline_integration` | 全链路match_filter + TaggedContext + Warning + Metadata |
| `test_matcher_*` (12 个) | ToolNamePattern / ToolMatchFilter 匹配逻辑 |
| `test_precedence_*` (5 个) | 多源权限优先级裁决 |
| `test_context_deduplication` | ContextDeduplicator 去重 |
| `test_async_hook_dispatch` | AsyncAgentHook 并行调度 |
| `test_permission_request_override` | PermissionRequest — Override 覆盖 Allow→Ask 升级 |
| `test_permission_request_continue` | PermissionRequest — Continue 不干预默认决策 |
| `test_permission_denied_fire_and_forget` | PermissionDenied — fire-and-forget 审计调度 |
| `test_permission_denied_sources` | PermissionDenied — Rule/Classifier/User/Timeout 四种来源标记 |