AstroResearch/docs/architecture/agent/permission.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

716 lines
27 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.

# 权限系统 (Permission System)
Agent 权限系统采用**多层纵深防御**架构,从工具级 trait 约束到运行时的 Hook 拦截、路径沙箱、命令黑名单,形成 7 层安全防线。
---
## 整体架构
```mermaid
graph TD
subgraph L1["第一层:工具 Trait 约束"]
IB["InterruptBehavior<br/>(Cancel vs Block)"]
CS["is_concurrency_safe<br/>(并行安全声明)"]
CP["check_permissions<br/>(自定义 PermissionRule)"]
SA["causes_sibling_abort<br/>(兄弟中断)"]
end
subgraph L2["第二层PermissionChecker 规则引擎"]
Rules["有序规则链<br/>Deny → Allow → Ask → Default Allow"]
WC["通配符 '*' 全匹配"]
end
subgraph L3["第三层Hook 管道"]
PreH["PreToolUse<br/>Continue | Block | MutateInput | PermissionRequired"]
PostH["PostToolUse<br/>Continue | MutateOutput"]
Builtins["3 内置 Hook<br/>Cancellation | Metrics | AuditLog"]
end
subgraph L4["第四层:文件系统沙箱"]
PS["路径沙箱<br/>library_dir / skills_dir / cwd"]
PT["穿越防护<br/>拒绝 .. 和 ~"]
end
subgraph L5["第五层Bash 命令安全"]
Blacklist["黑名单 21 条<br/>交互式/破坏性命令"]
Timeout["超时控制 60s (max 120s)"]
OutputLimit["输出截断 4000 字符"]
end
subgraph L6["第六层:子代理隔离"]
SilentCtx["Silent 上下文<br/>禁止 ask_user"]
FreshMsg["全新消息上下文<br/>不污染父代理"]
InheritPerm["继承 PermissionChecker<br/>+ HookRegistry"]
end
subgraph L7["第七层:运行时安全约束"]
MaxSteps["max_steps 上限"]
DupDetect["同质调用检测"]
TokenBudget["Token 预算 diminishing returns"]
Cancel["用户取消 (cancelled_runs)"]
end
AgentRuntime["AgentRuntime::run_turn()"] --> L1
L1 --> L2
L2 --> L3
L3 --> L4
L4 --> L5
L5 --> L6
L6 --> L7
```
---
## 第一层:工具级 Trait 约束
每个工具通过覆写 `AgentTool` trait 的 4 个安全方法声明自身行为边界。
定义位置:`src/agent/tools/mod.rs:179`
### 方法说明
| 方法 | 默认值 | 作用 |
|---|---|---|
| `interrupt_behavior()` | `Cancel` | 用户取消时的响应:`Cancel` 立即停止(只读工具),`Block` 等待完成(有副作用的写入工具) |
| `is_concurrency_safe(args)` | `false` | 是否可与其他工具并行执行。保守默认,只读工具须显式覆写为 `true` |
| `check_permissions(args)` | 空 `Vec` | 返回 `PermissionRule` 列表,由 PermissionChecker 运行时逐条匹配 |
| `causes_sibling_abort()` | `false` | 该工具失败时是否中止兄弟并行执行(下载/解析类工具可设为 `true` |
### 工具安全分类表
#### 只读并发安全工具
| 工具 | 域 | `is_concurrency_safe` | `interrupt_behavior` |
|---|---|---|---|
| `search_papers` | astro/search | `true` | `Cancel` |
| `get_paper_metadata` | astro/search | `true` | `Cancel` |
| `get_paper_content` | astro/paper | `true` | `Cancel` |
| `rag_search` | astro/rag | `true` | `Cancel` |
| `query_target` | astro/target | `true` | `Cancel` |
| `read_file` | filesystem | `true` | `Cancel` |
| `grep_files` | filesystem | `true` | `Cancel` |
| `glob_files` | filesystem | `true` | `Cancel` |
| `load_skill` | skill | `true` | `Cancel` |
#### 写入/IO 串行安全工具
| 工具 | 域 | `is_concurrency_safe` | `interrupt_behavior` |
|---|---|---|---|
| `download_paper` | astro/paper | `false` | `Cancel` |
| `parse_paper` | astro/paper | `false` | `Cancel` |
| `file_write` | filesystem | `false` | `Cancel` |
| `file_edit` | filesystem | `false` | `Cancel` |
| `save_note` | astro/note | `false` | `Cancel` |
| `save_memory` | memory | `false` | **`Block`** |
| `run_bash` | filesystem | `false` | **`Block`** |
| `ask_user` | ask_user | `false` | **`Block`** |
| `todo_write` | todo | `false` | `Cancel` |
| `compress_context` | compress | `false` | `Cancel` |
| `subagent` | subagent | `false` | **`Block`** |
> **设计原理**`Block` 工具在用户取消时忽略中断信号,确保写操作完整提交后才停止。`save_memory` 和 `run_bash` 涉及文件系统修改,`ask_user` 依赖 oneshot 通道生命周期,`subagent` 开启了完整的子代理 ReAct 循环,中断可能导致数据不一致。
---
## 第二层PermissionChecker 规则引擎
参考 Claude Code 的 PermissionChecker 设计,提供可编程的权限规则链。
定义位置:`src/agent/runtime/permission.rs`
### 权限判定规则
```
┌────────────────────────────────────────────────────────────┐
│ 规则匹配优先级 (first-match-wins) │
│ │
│ 1. Deny { tool_name, reason } — 不可覆盖的拒绝 │
│ 2. Allow { tool_name } — 显式允许 │
│ 3. Ask { tool_name, message } — 需要用户确认 │
│ 4. (default) — 无匹配 → Allow │
│ │
│ 通配符 "*" 匹配所有工具名 │
└────────────────────────────────────────────────────────────┘
```
### 多源权限优先级 (Multi-Source Permission Precedence)
当多个来源Checker 规则、Hook、工具级规则、会话规则同时做出权限决策时
`resolve_permission_precedence()` 按以下优先级裁决:
| Priority | Source | Description |
|----------|--------|-------------|
| **P0** (highest) | `PermissionChecker::Deny` | 环境变量/配置文件配置的 Deny 规则,不可覆盖 |
| **P1** | Tool-level `PermissionRule::Deny` | 工具自身拒绝执行(如 Bash 危险命令) |
| **P2** | Session-level `PermissionChecker::Deny` | API 动态添加的会话级 Deny |
| **P3** | Hook `PreToolUseAction::Block` | Hook 主动阻止工具执行 |
| **P4** | Hook `PermissionRequired` | 仅当 Checker 返回 Allowed 时升级为 AskUser |
| **P5** | Session-level `PermissionChecker::Ask` | 仅当当前为 Allowed 时升级 |
| **P6** | Tool-level `PermissionRule::Ask` | 仅当当前为 Allowed 时升级 |
| **P7** | `PermissionChecker::Allow` | 显式 Allow 规则 |
| **P8** (lowest) | Implicit Allow (default) | 无任何规则匹配 → 允许 |
**关键规则:**
- **Deny 不可覆盖**: P0-P2 的 Deny 规则在任何情况下生效
- **Ask 可升级**: P4-P6 在 Allow 状态下升级为 Ask在 Deny 状态下被忽略
- **Block = Deny**: Hook Block 等同于 Deny由 P0-P2 可覆盖
- **冲突日志**: `conflict_log` 记录所有被覆盖的决策,用于审计```
### PermissionChecker API
```rust
// src/agent/runtime/permission.rs
pub struct PermissionChecker {
rules: Vec<PermissionRule>, // 有序规则列表,先添加的优先级更高
}
impl PermissionChecker {
pub fn new() -> Self; // 空检查器 = 默认允许所有
pub fn add_rule(&mut self, rule: PermissionRule); // 添加规则
pub fn check(&self, tool_name: &str) -> PermissionResult; // 检查单个工具
pub fn is_denied(&self, tool_name: &str) -> bool; // 快捷 deny 检查
fn matches(pattern: &str, tool_name: &str) -> bool {
pattern == "*" || pattern == tool_name // 通配符或精确匹配
}
}
pub enum PermissionResult {
Denied { reason: String },
Allowed,
AskUser { message: String },
}
```
### 使用示例
```rust
// 创建受限检查器:只允许只读操作
let mut checker = PermissionChecker::new();
checker.add_rule(PermissionRule::Deny {
tool_name: "run_bash".into(),
reason: "此会话中禁止执行命令".into(),
});
checker.add_rule(PermissionRule::Deny {
tool_name: "download_paper".into(),
reason: "禁止下载".into(),
});
checker.add_rule(PermissionRule::Ask {
tool_name: "file_write".into(),
message: "是否允许写入文件?".into(),
});
// 默认 Allow — 其余工具正常执行
assert!(checker.check("search_papers").is_allowed());
assert!(checker.check("run_bash").is_denied());
```
### 当前集成状态
```mermaid
graph LR
subgraph "AgentConfig::from_env_optional()"
PC0["解析 AGENT_PERMISSIONS_* 环境变量<br/>构造 PermissionChecker::from_config()"]
end
subgraph "AgentRuntime::run_turn()"
PC1["permission_checker: PermissionChecker<br/>(Deny → Ask → Allow 规则链 + 4种模式)"]
end
subgraph "SubAgentRunner"
PC2["permission_checker: Arc&lt;PermissionChecker&gt;"]
PC2 -->|"check(tool_name, Some(&args))"| SubExec["三态检查<br/>Deny→注入错误 | Ask→自动拒绝 | Allow→执行"]
end
subgraph "execute_parallel() Phase 2.5"
PC3["permission_checker: Option&lt;&PermissionChecker&gt;"]
PC3 -->|"check() + apply_mode() + 工具级叠加"| DenyCheck["━━ 三态处理 ━━<br/>Deny → 注入错误跳过执行<br/>AskUser → SSE PermissionRequest + oneshot 等待(120s超时)<br/>Allowed → 正常进入执行队列"]
end
PC0 -.->|"构建"| PC1
PC1 -.->|"传递给"| PC2
PC1 -.->|"传递给"| PC3
style PC3 fill:#ccffcc,stroke:#00aa00
```
> **✅ 完整已实现**`executor::execute_parallel()` 在 Phase 2.5PreToolUse hooks 之后、工具执行之前)执行完整的权限检查管道:
> 1. `PermissionChecker::check(tool_name, tool_args)` — 规则链匹配 + 内容级匹配
> 2. `PermissionChecker::apply_mode()` — 模式变换Bypass/DontAsk/AcceptEdits
> 3. Hook `PermissionRequired` 升级 — hook 请求的权限确认为 AskUser
> 4. 工具级 `check_permissions()` 叠加 — 工具自定义规则在 Allow 时升级为 Ask
> 5. 最终三态分流Deny → 注入错误 / AskUser → oneshot 交互(120s 超时) / Allowed → 正常执行
---
## 第三层Hook 管道
Hook 系统提供了可编程的事件拦截点,参考 Claude Code 的 PreToolUse/PostToolUse/Stop hooks 设计。
定义位置:`src/agent/hooks.rs`
### PreToolUse 动作类型
```mermaid
graph TD
PreToolUse["PreToolUse hook"]
PreToolUse --> Continue["Continue<br/>正常执行"]
PreToolUse --> Block["Block { reason }<br/>阻止执行,第一个 Block 短路整个链"]
PreToolUse --> Mutate["MutateInput { updated_args, additional_context }<br/>修改参数 + 注入附加上下文"]
PreToolUse --> PermReq["PermissionRequired { permission, tool_name }<br/>请求权限决策Phase 2 待完善)"]
```
### PostToolUse 动作类型
```mermaid
graph TD
PostToolUse["PostToolUse hook"]
PostToolUse --> Continue2["Continue<br/>保持输出不变"]
PostToolUse --> MutateOut["MutateOutput { updated_content }<br/>修改工具输出(如脱敏)"]
```
### Hook 链执行逻辑 (`run_pre_tool_use`)
```rust
// 遍历所有已注册 hook
for hook in &self.hooks {
let action = hook.pre_tool_use(ctx).await;
match action {
Block { reason } => {
// 第一个 Block 立即短路返回,不执行后续 hook
return PreToolUseResult { action, ... };
}
MutateInput { updated_args, additional_context } => {
// 累积 additional_context多 hook 拼接)
// 更新 final_args最后一个 MutateInput 的修改生效)
}
PermissionRequired { .. } => {
// 记录日志但暂不阻塞Phase 2 完善)
}
Continue => {} // 继续下一个 hook
}
}
```
### 3 个内置 Hook
```mermaid
classDiagram
class CancellationHook {
-cancelled_runs: Arc~Mutex~HashSet~String~~
+pre_tool_use() → Block | Continue
+on_session_stop() → 清理取消状态
}
class MetricsHook {
-data: Arc~Mutex~MetricsData~
+on_session_start() → 关联 session_id
+post_tool_use() → 累计工具调用/错误计数
+on_step_complete() → 每 3 步输出摘要日志
+on_session_stop() → 输出终止原因
+snapshot() → 返回可查询的指标快照
}
class AuditLogHook {
-db: SqlitePool
+post_tool_use() → fire-and-forget 写入 agent_audit_log
+on_session_stop() → 写入 SESSION_STOP 标记
}
class AgentHook {
<<interface>>
+name() &str
+on_session_start()
+pre_tool_use()
+post_tool_use()
+on_step_complete()
+on_session_stop()
+on_subagent_start()
+on_subagent_stop()
+on_pre_compact()
+on_post_compact()
}
AgentHook <|-- CancellationHook
AgentHook <|-- MetricsHook
AgentHook <|-- AuditLogHook
```
### 审计日志 (`agent_audit_log` 表)
`AuditLogHook` 在每次工具执行后通过 fire-and-forget`tokio::spawn`)写入审计记录:
| 字段 | 说明 |
|---|---|
| `session_id` | 会话 ID |
| `step` | ReAct 步数 |
| `tool_name` | 工具名称 |
| `status` | `"OK"``"FAIL"` |
| `elapsed_ms` | 执行耗时(毫秒) |
| `output_preview` | 输出内容前 200 字符 |
| `agent_name` | 代理身份(`"lead"` 或子代理名) |
会话终止时写入一条 `tool_name = 'session'`、`status = 'SESSION_STOP'` 的汇总记录。
---
## 第四层:文件系统路径沙箱
所有文件操作工具(`read_file`、`file_write`、`file_edit`、`glob_files`、`grep_files`、`run_bash` 的 `working_dir` 参数)共享的路径安全检查。
定义位置:`src/agent/tools/filesystem/security.rs`
### 允许的根目录
```rust
let allowed_roots = [
config.library_dir.canonicalize(), // 论文库目录
config.skills_dir.canonicalize(), // Agent Skills 目录
std::env::current_dir(), // 项目根目录
];
```
### 安全检查函数
```mermaid
flowchart TD
Input["用户提供的路径字符串"] --> PT{"has_path_traversal()<br/>包含 .. 或 ~ "}
PT -->|是| Reject1["❌ 拒绝"]
PT -->|否| Resolve["resolve_path()<br/>绝对路径直接用,相对路径基于 cwd 拼接"]
Resolve --> Canon["canonicalize()<br/>消除符号链接"]
Canon -->|失败| TryParent["尝试对父目录 canonicalize"]
TryParent -->|失败| Reject2["❌ 拒绝:无法解析"]
Canon -->|成功| Check{"is_path_allowed()<br/>在 allowed_roots 内?"}
TryParent -->|成功| Check
Check -->|是| Allow["✅ 允许"]
Check -->|否| Reject3["❌ 拒绝:无权访问"]
```
### 防护能力
| 攻击类型 | 防护方式 |
|---|---|
| 路径穿越 (`../../../etc/passwd`) | `has_path_traversal()` 拒绝含 `..` 的路径 |
| 家目录访问 (`~/`) | `has_path_traversal()` 拒绝含 `~` 的路径 |
| 符号链接逃逸 | `canonicalize()` 解析符号链接到真实路径后再检查 |
| 绝对路径越界 | `resolve_path()` 解析后再 `is_path_allowed()` |
### 覆盖的工具
| 工具 | 受保护的参数 |
|---|---|
| `read_file` | `file_path` |
| `file_write` | `file_path` |
| `file_edit` | `file_path` |
| `glob_files` | `pattern` (解析后) |
| `grep_files` | `path` |
| `run_bash` | `working_dir` |
---
## 第五层Bash 命令安全校验
`run_bash` 工具在路径沙箱之上叠加了命令级安全校验。
定义位置:`src/agent/tools/filesystem/bash.rs`
### 校验流程
```mermaid
flowchart TD
Cmd["command 参数"] --> Trim["trim() 去空格"]
Trim --> Empty{"空命令 / bash / bash -c"}
Empty -->|是| Reject1["❌ 拒绝"]
Empty -->|否| Lower["to_lowercase()"]
Lower --> Blacklist{"黑名单子串匹配<br/>21 条模式)"}
Blacklist -->|命中| Reject2["❌ 拒绝:不允许执行 'X' 类命令"]
Blacklist -->|未命中| Execute["✅ 执行"]
```
### 黑名单(精确首词匹配,已修复子串误伤问题)
校验流程:
```mermaid
flowchart TD
Cmd["command 参数"] --> Trim["trim() 去空格"]
Trim --> Empty{"空命令 / bash / bash -c"}
Empty -->|是| Reject1["❌ 拒绝"]
Empty -->|否| Bypass{"命令替换绕过?<br/>$() 或反引号开头的首词"}
Bypass -->|是| Reject2["❌ 拒绝"]
Bypass -->|否| Extract["extract_first_command_word()<br/>提取首个命令单词"]
Extract --> Whitelist{"SAFE_COMMANDS 白名单?<br/>35 个安全命令)"}
Whitelist -->|是| Allow["✅ 直接允许"]
Whitelist -->|否| Blacklist{"DANGEROUS_COMMANDS 黑名单?<br/>33 个精确匹配)"}
Blacklist -->|是| Reject3["❌ 拒绝"]
Blacklist -->|否| ArgCheck{"DANGEROUS_ARG_PATTERNS<br/>5 个危险参数子串)"}
ArgCheck -->|命中| Reject4["❌ 拒绝"]
ArgCheck -->|未命中| Allow2["✅ 默认允许<br/>(路径沙箱 + 超时兜底)"]
```
**精确匹配 vs 子串匹配(修复前/后对比)**
| 命令 | 修复前(子串) | 修复后(首词精确) |
|---|---|---|
| `grep "ssh_config" *.rs` | ❌ 误拦(含子串 `ssh ` | ✅ 允许(首词 `grep` 在白名单) |
| `echo "use sudo carefully"` | ❌ 误拦(含子串 `sudo ` | ✅ 允许(首词 `echo` 在白名单) |
| `cat /usr/share/vim/vimrc` | ❌ 误拦(含子串 `vim ` | ✅ 允许(首词 `cat` 在白名单) |
| `python script.py` | ✅ 允许 | ⚠️ 通过校验,但触发 `Ask` 用户确认(不在白名单) |
| `vim file.txt` | ✅ 拒绝 | ✅ 拒绝(首词 `vim` 在黑名单) |
| `$(echo sud; echo o) /etc/passwd` | ✅ 允许(绕过!) | ❌ 拒绝(检测到命令替换绕过) |
### 安全白名单(已启用)
```rust
const SAFE_COMMANDS: &[&str] = &[
"ls", "cat", "head", "tail", "find", "grep", "wc", "echo",
"pwd", "sort", "uniq", "cut", "tr", "awk", "sed", "jq",
"diff", "file", "stat", "du", "df", "env", "printenv",
"which", "basename", "dirname", "realpath", "readlink",
"xargs", "tee", "date", "sleep", "true", "false",
];
```
白名单内的命令**优先放行**,不经过黑名单检查。非白名单命令经黑名单精确匹配和危险参数检查后,通过 `bash_needs_permission()``RunBashTool::check_permissions()` 返回 `Ask` 规则触发用户确认弹窗PermissionRequestCard
### 其他约束
| 约束 | 值 | 说明 |
|---|---|---|
| 超时 | 默认 60s最大 120s | `AGENT_TOOL_TIMEOUT_SECS` 环境变量 |
| 输出截断 | 4000 字符 | `truncate_content()` |
| 工作目录 | library_dir / skills_dir / cwd | 受路径沙箱约束 |
### 已知局限
1. ~~**黑名单子串匹配**~~ — ✅ 已修复:改用首词精确匹配,`grep "ssh_config"` 不再误拦
2. ~~**未限制网络访问**~~ — ✅ 已修复:`NETWORK_COMMANDS` 名单curl/wget/nc/socat 等)默认阻止,`AGENT_BLOCK_NETWORK=false` 可放行
3. **未限制进程数** — fork bomb`:(){ :\|:& };:`)未被检测
4. **管道/重定向完整放行**`<`、`>`、`|` 不做限制
5. ~~**`$()` 命令替换**~~ — ✅ 已修复:检测首词位置 `$()` 和反引号绕过
6. ~~**白名单未被使用**~~ — ✅ 已修复:`SAFE_COMMANDS` 已集成到 `validate_bash_command()` 中,白名单命令优先放行
---
## 第六层:子代理隔离
子代理通过 `SubAgentRunner` 创建上下文隔离的执行环境,参考 Claude Code Subagents 设计。
定义位置:`src/agent/subagent.rs`、`src/agent/tools/subagent.rs`
### 隔离维度对比
| 维度 | 父代理 | 子代理 |
|---|---|---|
| 消息上下文 | 完整历史 + 所有中间工具调用 | **全新 messages**:仅 `[system_prompt, user_prompt]` |
| 工具注册表 | 完整 `ToolRegistry`19+ 工具) | 完整 `ToolRegistry`(共享同一个引用) |
| 工具上下文 | `ToolContext::new()` | `ToolContext::silent()`**`silent = true`** |
| Hook 管道 | 完整 `HookRegistry` | 继承父代理的 `HookRegistry` |
| PermissionChecker | 自己的实例 | 继承父代理的 `PermissionChecker` |
| 上下文压缩 | 多层压缩micro/auto/manual | 独立的自动压缩 |
| 死循环检测 | `DuplicateDetector` per turn | 独立的 `(last_call, consecutive_count)` |
### Silent 模式
```rust
// src/agent/tools/mod.rs:114
pub fn silent(app_state: Arc<AppState>) -> Self {
ToolContext {
app_state,
session_id: String::new(),
silent: true, // ← 关键标志
read_file_state: ...,
sse_tx: None, // ← 无 SSE 通道
enable_thinking: false,
}
}
```
**Silent 模式效果**
- `ask_user` 工具检测到 `ctx.silent == true` 时**直接返回错误**
- 阻止子代理绕过父代理向终端用户提问
- 无 SSE 通道 → 子代理的工具调用进度不会单独推送到前端
### 子代理内部权限流程
```mermaid
sequenceDiagram
participant SA as SubAgentRunner
participant Hook as HookRegistry
participant PC as PermissionChecker
participant Tool as AgentTool
SA->>Hook: PreToolUseContext { tool_name, args }
Hook-->>SA: PreToolUseResult { action, final_args }
alt action = Block
SA-->>SA: 注入错误 tool_result跳过执行
else action = Continue / MutateInput
SA->>PC: is_denied(tool_name)
alt 被拒绝
SA-->>SA: 注入错误 tool_result跳过执行
else 允许
SA->>Tool: execute(final_args, silent_ctx)
Tool-->>SA: ToolOutput
SA->>Hook: PostToolUse hooks → 可能修改输出
end
end
```
> **⚠️ 当前局限**:子代理使用**完整的父代理 ToolRegistry**,不支持按子任务需求裁剪工具列表(如"仅搜索"模式只给只读工具)。未来可引入 `ToolRegistry::restrict()` 方法实现最小权限原则。
---
## 第七层:运行时安全约束
ReAct 循环中的多层终止条件,防止无限循环和资源耗尽。
定义位置:`src/agent/runtime/mod.rs:442`
### 终止条件矩阵
| 条件 | 触发阈值 | 行为 |
|---|---|---|
| **最大步数** | `step > max_steps` (默认 8) | 强制 LLM 生成最终答案(不带工具调用),注入提醒消息 |
| **同质调用** | 连续 3 次相同 `(tool_name, args)` | 注入错误 tool_result跳过本轮该工具 |
| **Token diminishing returns** | 连续多步无新增信息 | 强制结束,注入"请基于已收集信息直接回答" |
| **用户取消** | `cancelled_runs` 含当前 `session_id` | 循环开始和工具执行中双重检查,发送 Error SSE 事件 |
| **压缩熔断** | `CompactionCircuitBreaker` 连续失败 | 跳过自动压缩,避免无限压缩循环 |
| **Token 硬限制** | `token_hard_limit` (默认 40000) | `TokenBudget` 触发强制动作 |
### 用户取消的双重检查
```mermaid
sequenceDiagram
participant User as 用户
participant API as API 层
participant State as cancelled_runs
participant Loop as ReAct 循环
participant Exec as 工具执行
User->>API: POST /api/chat/cancel
API->>State: insert(session_id)
Note over Loop: 每步迭代开始
Loop->>State: contains(session_id)?
State-->>Loop: true → break 循环
Note over Exec: 工具执行中 (每 250ms)
loop 取消轮询
Exec->>State: contains(session_id)?
State-->>Exec: true → interrupt
end
Note over Exec: InterruptBehavior 判断
alt interrupt_behavior = Cancel
Exec-->>Exec: 立即停止,返回 "执行已被用户取消"
else interrupt_behavior = Block
Exec-->>Exec: 忽略中断信号,等待完成
end
```
- 主循环在**每步开始**检查取消标志
- 工具执行器以 **250ms 间隔**轮询取消状态
- `Block` 工具(`run_bash`、`save_memory`、`ask_user`、`subagent`)忽略取消信号直到自然完成
### 取消状态生命周期
1. 用户通过 API 端点设置 `cancelled_runs.insert(session_id)`
2. `CancellationHook::pre_tool_use()` 检测到 → 返回 `Block`
3. ReAct 循环入口检测到 → `break` 跳出
4. 工具执行检测到 + `InterruptBehavior::Cancel` → 立即返回
5. `CancellationHook::on_session_stop()` → 清理 `cancelled_runs.remove(session_id)`
---
## 配套安全机制
### Background Task 安全
位置:`src/agent/tools/background.rs`、`src/agent/background.rs`
- `bg_task_run` 用于在后台执行慢速操作(下载、解析)
- 后台任务通过 `BgNotificationQueue` 注入结果,不直接访问 Agent 上下文
- 结果注入在下一次 LLM 调用前以 user 消息形式推送
### 工具输出持久化
位置:`src/agent/tools/persist.rs`
- 大型工具结果(超过 `max_tool_output_chars`)写入磁盘,消息中只包含 stub
- 写入路径:`{library_dir}/tool-results/{tool_call_id}.txt`
- 通过调用外部工具读取完整结果,避免上下文污染
### Token 预算管理
位置:`src/agent/runtime/token_budget.rs`
```
软限制 (token_soft_limit, 默认 32000)
↓ 触发渐进式 nudging 提醒 → 建议 LLM 总结/给出答案
硬限制 (token_hard_limit, 默认 40000)
↓ 触发强制动作 → 上下文压缩或强制结束
Diminishing Returns 检测
↓ 连续无新增信息 → 强制结束 + 直接回答
```
---
## 权限检查全链路
一次完整的工具调用穿越全部 7 层防线:
```
工具调用请求
├─ [L7] 循环入口:步数 / 取消 / diminishing returns 检查
├─ [L7] validate_and_prepare():死循环检测 + 参数解析
├─ [L3] PreToolUse hooks
│ ├── CancellationHook → 检查 cancelled_runs
│ ├── 自定义 Hook → Block? MutateInput?
│ └── 返回 final_args + additional_context
├─ [L2] PermissionChecker.check() ← ✅ 在 Phase 2.5 调用PreToolUse hooks 之后、执行之前)
│ ├── Deny → 注入错误 result跳过执行不进入队列
│ ├── AskUser → 暂视为允许Phase 2 确认交互待完善)
│ └── Allowed → 正常进入执行队列
├─ [L1] 分区器 (ToolPartitioner)
│ └── is_concurrency_safe() 判断 → 并行 or 串行批次
├─ 工具执行 (每工具独立 Future)
│ │
│ ├─ [L1] InterruptBehavior 判断 → Cancel 可中断 / Block 不可中断
│ │
│ ├─ [L4] 路径沙箱 (read_file / file_write / file_edit / glob / grep / bash)
│ │
│ ├─ [L5] Bash 黑名单 (run_bash):
│ │ ├── validate_bash_command() → 空命令 / 黑名单
│ │ ├── 工作目录路径沙箱检查
│ │ └── 超时控制 (60s default / 120s max)
│ │
│ ├─ [L6] ask_user Silent 模式检查 → 子代理中直接返回错误
│ │
│ └─ 超时控制 (AGENT_TOOL_TIMEOUT_SECS, default 120s)
└─ [L3] PostToolUse hooks
├── 输出截断 + 大结果持久化
├── MetricsHook → 累计指标
├── AuditLogHook → fire-and-forget 审计日志
└── MutateOutput → 输出修改
```
---
## 待完善项
| 优先级 | 项目 | 当前状态 | 建议 |
|---|---|---|---|
| **MEDIUM** | Auto Mode (AI 分类器) | 未实现 | LLM 评估风险,快速路径 + 安全工具白名单 |
| **LOW** | 文件写入大小限制 | 无上限 | 添加 `max_file_size` 参数 |