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 样式变量化
922 lines
32 KiB
Markdown
922 lines
32 KiB
Markdown
# 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 — 内置 Hooks(CancellationHook、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<HookEvent, Vec<usize>><br/>预计算索引"]
|
||
AIndex["async_event_index<br/>异步 hook 索引"]
|
||
SH["session_hooks<br/>HashMap<String, Vec<Box<dyn AgentHook>><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["内置 Hooks(3 个 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<dyn AgentHook>"] --> 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<&dyn AgentHook>"]
|
||
FI --> Merge
|
||
SI --> Merge
|
||
end
|
||
|
||
Register --> Index["event_index: HashMap<HookEvent, Vec<usize>>"]
|
||
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** | 独立 trait,fire-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 四种来源标记 |
|