AstroResearch/docs/architecture/agent/permission.md
Asfmq f6df9d8136 feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构
- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking
  开关,仅千问/DashScope 时启用
  - 完善权限系统,支持细粒度的权限控制和用户权限申请
  - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构
  - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识
  - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name
  - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段
  - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参
  - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段
  - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化
  - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放
  - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义
  - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档
  - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南
  - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
2026-06-18 01:21:02 +08:00

942 lines
39 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 │
│ │
│ 通配符 "*" 匹配所有工具名 │
└────────────────────────────────────────────────────────────┘
```
### 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` | ✅ 允许 | ✅ 允许(不在黑名单,默认允许) |
| `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",
];
```
白名单内的命令**优先放行**,不经过黑名单检查。非白名单命令经过黑名单精确匹配和危险参数二次检查后默认允许。
### 其他约束
| 约束 | 值 | 说明 |
|---|---|---|
| 超时 | 默认 60s最大 120s | `AGENT_TOOL_TIMEOUT_SECS` 环境变量 |
| 输出截断 | 4000 字符 | `truncate_content()` |
| 工作目录 | library_dir / skills_dir / cwd | 受路径沙箱约束 |
### 已知局限
1. ~~**黑名单子串匹配**~~ — ✅ 已修复:改用首词精确匹配,`grep "ssh_config"` 不再误拦
2. **未限制网络访问**`curl`、`wget` 不在黑名单中
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 → 输出修改
```
---
## Claude Code 权限系统对比分析
> 对比基准Claude Code (`/home/fmq/program/claudecode/src/utils/permissions/`)
> 分析日期2026-06-17
### 架构差异总览
| 维度 | AstroResearch (当前) | Claude Code (参考) | 差距 |
|------|---------------------|-------------------|------|
| 规则引擎 | ✅ PermissionChecker (完成) | ✅ hasPermissionsToUseTool 多步流水线 | 相当 |
| 规则匹配粒度 | ✅ 内容级 `Tool(content*)` 前缀/后缀/包含 | ✅ 前缀/通配/内容级 / 正则 | 小 |
| AskUser 交互流 | ✅ SSE → PermissionRequestCard → Allow/Deny/Always Allow | ✅ 完整 SSE → Dialog → 决策 | 相当 |
| 权限模式 | ✅ Default/AcceptEdits/Bypass/DontAsk | ✅ 6种模式 (含 plan/auto) | 小 |
| 规则持久化 | ✅ 环境变量加载 + `PermissionChecker::from_config()` | ✅ settings.json 多层加载 (8级来源优先级) | 小 |
| 规则来源追踪 | ✅ `PermissionRuleSource` 枚举 (Env/Session) | ✅ cliArg > command > session > userSettings > ... | 小 |
| Bash 权限分类器 | ✅ SAFE_COMMANDS 白名单 + `check_permissions()` 集成 | ✅ AST解析 + AI分类器 + 异步推测 | 中等 |
| 拒绝追踪/熔断 | ✅ `DenialTracker` 连续/累计计数 + ReAct 循环熔断 | ✅ 连续/总计拒绝计数 + 自动终止 | 相当 |
| 权限 Hook 集成 | ✅ PreToolUseAction::PermissionRequired 完整流程 | ✅ 完整 PermissionRequest hook + 多路径决议 | 相当 |
| 规则遮蔽检测 | ✅ `detect_shadowed_rules()` deny/ask 双重检查 | ✅ `shadowedRuleDetection` deny/ask 遮蔽检测 | 相当 |
| Auto Mode (AI 分类) | ❌ 无 | ✅ YOLO classifier + 快速路径 + 安全工具白名单 | **远期** |
| 权限解释器 | ✅ 启发式 `explain_permission()` (Bash 风险等级 + 路径检测) | ✅ Haiku 生成风险解释 | 中等 |
| 会话内规则更新 | ✅ `POST/PUT /api/chat/sessions/:id/permissions/*` | ✅ `/permissions` 命令 + API | 小 |
| 子代理权限继承 | ✅ 完整 `check()` 三态检查 | ✅ 完整继承父级权限上下文 | 相当 |
| 附加目录沙箱 | ✅ `AGENT_ADDITIONAL_DIRS` + `is_path_allowed()` 扩展 | ✅ `additionalDirectories` 可配置 | 相当 |
### Claude Code 权限流水线 (参考架构)
```
hasPermissionsToUseTool(toolName, input, context):
Step 1a: 工具级 deny 规则检查 → deny → 返回 deny
Step 1b: 工具级 ask 规则检查 → ask → 返回 ask (sandbox 例外)
Step 1c: 工具自定义 checkPermissions() → 内容级规则匹配
Step 1d: 工具实现返回 deny → deny → 返回 deny
Step 1e: requiresUserInteraction? → ask → 强制 ask (bypass 免疫)
Step 1f: 内容级 ask 规则 → ask → 强制 ask (bypass 免疫)
Step 1g: 安全检查 (敏感路径等) → ask → 强制 ask (bypass 免疫)
Step 2a: bypassPermissions 模式? → allow → 返回 allow
Step 2b: 工具级 allow 规则 → allow → 返回 allow
Step 3: 剩余 passthrough → ask → 返回 ask
外层模式变换:
dontAsk 模式: ask → deny
auto 模式: acceptEdits 快速路径 → 安全工具白名单 → AI分类器
headless: hooks 先运行 → 无 hook 决定 → auto-deny
```
### 关键设计决策对比
**1. 规则格式**
Claude Code 使用 `ToolName(content)` 格式支持内容级规则:
```
Bash → 匹配所有 bash 命令
Bash(npm install) → 匹配精确命令
Bash(npm *) → 前缀通配
Bash(rm:*) → 旧版前缀(已废弃)
Read(.env) → 文件模式
mcp__server__tool → MCP 工具级
mcp__server → MCP 服务级
Agent(Explore) → 代理类型级
```
AstroResearch 已实现相同格式:
```
"*" → 通配所有工具
"tool_name" → 精确工具名匹配
"tool_name(content*)" → 前缀通配(如 "run_bash(rm *)" 匹配 "rm -rf /"
"tool_name(*suffix)" → 后缀通配(如 "read_file(*.env)" 匹配 ".env"
"tool_name(exact)" → 包含匹配(子串命中)
"*(content)" → 工具通配 + 内容匹配(如 "*(sudo)" 匹配任意工具的 sudo 命令)
```
从 args 中自动提取 `command`/`file_path`/`path`/`pattern`/`url` 字段进行内容匹配。
**2. 权限模式**
Claude Code 的 6 种模式通过 Shift+Tab 循环切换:
- `default` — 标准逐项确认
- `acceptEdits` — 工作目录内文件编辑自动通过
- `bypassPermissions` — 跳过所有 Askdeny/ask 规则仍生效;安全检查 bypass 免疫)
- `dontAsk` — 所有 Ask 转 Deny
- `plan` — 计划模式
- `auto` — AI 自动分类(内部使用)
AstroResearch 已实现 4 种模式(通过 `AGENT_PERMISSION_MODE` 环境变量或 API 切换):
- `default` — 标准规则链Ask 触发用户交互
- `acceptEdits` — 工作目录内文件编辑自动通过(路径检查由 executor 完成)
- `bypassPermissions` — 跳过所有 AskDeny 规则仍生效)
- `dontAsk` — 所有 Ask 转为 Deny
`PermissionChecker::from_config()``AgentConfig` 加载环境变量规则并构造完整检查器。
**3. 多路径权限决议**
Claude Code 的 AskUser 决议支持多个并行路径,任一先返回即生效(`claim()` 模式):
- 本地 UI 对话框
- Bridge 响应CCR 远程)
- Channel 响应Telegram 等)
- PermissionRequest hooks后台异步运行
- Bash 分类器(后台推测性异步分类)
AstroResearch 已实现完整的 AskUser 交互流:
- `PermissionChecker::check()` 返回 `AskUser`executor 发送 `AgentStreamEvent::PermissionRequest` SSE 事件
- 通过 `oneshot` 通道创建 `PendingPermission`,存入 `AppState::pending_permissions`
- 等待用户通过前端 `PermissionRequestCard` 组件响应Allow / Deny / Always Allow120s 超时自动拒绝
- 单一路径决议oneshot不支持多路径 claim 模式
---
## 优化路线图
### ✅ P0 — 已全部完成
#### P0-1: 规则加载与持久化 ✅
`AgentConfig::from_env_optional()` 从环境变量加载规则(`AGENT_PERMISSIONS_DENY`/`ALLOW`/`ASK``PermissionChecker::from_config()` 按 Deny → Ask → Allow 优先级顺序构造规则链。
**实现位置**: `src/agent/runtime/mod.rs:113-117`, `src/agent/runtime/permission.rs:268-290`
#### P0-2: 完成 AskUser 权限交互流 ✅
executor Phase 2.5 中完整的 AskUser 处理:
- `AgentStreamEvent::PermissionRequest` SSE 事件 → 前端 `PermissionRequestCard` 组件
- `oneshot` 通道 + `AppState::pending_permissions` 存储
- 120s 超时自动拒绝,支持 Allow / Deny / Always Allow 决策
**实现位置**: `src/agent/runtime/executor.rs:282-422`, `src/api/agent.rs`, `dashboard/src/features/agent/PermissionRequestCard.tsx`
#### P0-3: 内容级权限匹配 ✅
`PermissionChecker::matches()` 支持 `"tool_name(content_pattern)"` 格式,前缀通配(`prefix*`)、后缀通配(`*suffix`)、包含匹配,自动从 args 提取 `command`/`file_path`/`path`/`pattern`/`url` 字段。
**实现位置**: `src/agent/runtime/permission.rs:183-255`
### ✅ P1 — 已全部完成
#### P1-1: 权限模式系统 ✅
`PermissionMode` 枚举实现 4 种模式Default/AcceptEdits/Bypass/DontAsk通过 `AGENT_PERMISSION_MODE` 环境变量或 API 切换。`PermissionChecker::apply_mode()` 在 executor Phase 2.5 中对检查结果进行模式变换Bypass 将 Ask→AllowedDontAsk 将 Ask→Denied
**实现位置**: `src/agent/runtime/permission.rs:39-60, 139-177`
#### P1-2: Bash 权限接入 PermissionChecker ✅
`RunBashTool::check_permissions()` 调用 `bash_needs_permission()` —— 安全白名单中的命令返回空规则(自动允许),非白名单命令返回 `Ask` 规则。executor Phase 2.5 中与 PermissionChecker 结果叠加。
**实现位置**: `src/agent/tools/filesystem/bash.rs:58-73, 362-365`
#### P1-3: 权限 Hook 集成 ✅
`PreToolUseAction::PermissionRequired` 在 executor 中被检测:若 hook 返回 `PermissionRequired` 且 PermissionChecker 返回 `Allowed`,则升级为 `AskUser` 触发用户交互。已修复 Continue 覆盖 meaningful action 的 bug。
**实现位置**: `src/agent/hooks.rs:378-384`, `src/agent/runtime/executor.rs:221-230`
#### P1-4: 子代理完整权限继承 ✅
`SubAgentRunner` 使用 `check(tool_name, Some(&final_args))` 进行三态检查Deny → 注入错误跳过执行AskUser → 自动拒绝子代理不应打断用户Allowed → 正常执行。
**实现位置**: `src/agent/subagent.rs`
### ✅ P2-1、P2-2 — 已实现
#### P2-1: 拒绝追踪与熔断 ✅
`DenialTracker` 追踪连续拒绝和总拒绝数,阈值触发 ReAct 循环终止。
配置:`AGENT_DENIAL_MAX_CONSECUTIVE` (默认 3) / `AGENT_DENIAL_MAX_TOTAL` (默认 20)。
**实现位置**: `src/agent/runtime/denial_tracker.rs`, `src/agent/runtime/mod.rs`
#### P2-2: 会话内规则更新 API ✅
`POST /api/chat/sessions/:id/permissions/rules` — add/remove 规则
`PUT /api/chat/sessions/:id/permissions/mode` — 切换权限模式
通过 `AppState::session_permission_checker` (`Arc<RwLock<PermissionChecker>>`) 实现跨 turn 共享。
**实现位置**: `src/api/permissions.rs`, `src/agent/runtime/executor.rs` Phase 2.5
### 🟡 P2-3~P2-5 — 远期增强(按需实现)
#### P2-3: 规则遮蔽检测 ✅
`PermissionChecker::detect_shadowed_rules()` 检测 Deny/Ask 遮蔽 Allow 的情况,输出 `ShadowedRule` 列表(含 reason + fix 建议),在 AgentRuntime 初始化时通过 `warn!` 日志输出。
**实现位置**: `src/agent/runtime/permission.rs`
#### P2-4: 权限解释器 ✅
启发式 `explain_permission()` 函数,根据工具名和参数生成 `{risk_level, explanation, reasoning, risk}` 结构。Bash 命令通过关键词检测风险等级HIGH/MEDIUM/LOW文件操作检测系统路径。结果随 `PermissionRequest` SSE 事件推送到前端。
**实现位置**: `src/agent/runtime/permission_explainer.rs`, `src/agent/runtime/mod.rs` `AgentStreamEvent::PermissionRequest.explanation`
#### P2-5: Auto Mode (AI 权限分类器) ❌
使用 LLM 自动评估工具调用的风险:
- 快速路径:`AcceptEdits` 模式自动允许工作目录内的文件编辑
- 安全工具白名单:`read_file`、`grep_files`、`search_papers` 等只读操作自动允许
- AI 分类:对不确定的操作调用快速模型判断安全性
- 失败封闭:分类器不可用时拒绝所有非白名单操作(安全优先)
**工作量**: 3-5天
#### P2-4: 权限解释器
在执行前用 LLM 生成人类可读的风险描述:
```
"该命令将执行 npm install可能修改 node_modules/ 目录并下载外部依赖包。"
```
**工作量**: 1天
#### P2-5: 子代理最小权限 (ToolRegistry::restrict)
```rust
impl ToolRegistry {
pub fn restrict(&self, allowed_tools: &[&str]) -> Self {
// 创建仅包含指定工具的受限注册表
}
}
```
**工作量**: 0.5天
---
## 实现路线图
```
已完成 (Phase 1): P0-1 规则加载 + P0-2 AskUser 交互流 + P0-3 内容级匹配
已完成 (Phase 2): P1-1 权限模式 + P1-2 Bash 集成 + P1-3 Hook 集成 + P1-4 子代理继承
已完成 (Phase 3): P2-1 拒绝追踪熔断 + P2-2 会话内规则更新 + P2-3 规则遮蔽检测 + P2-4 权限解释器 + 附加目录沙箱 + 规则来源追踪
远期规划 (按需): P2-5 Auto Mode (AI 分类器) + P2-6 权限解释器 LLM 升级 + 子代理最小权限 + 文件写入大小限制
```
---
## 待完善项
| 优先级 | 项目 | 当前状态 | 建议 |
|---|---|---|---|
| ~~**HIGH**~~ | ~~PermissionChecker 集成到主执行路径~~ | ✅ **已完成** | — |
| ~~**HIGH**~~ | ~~Bash 黑名单改为命令解析~~ | ✅ **已完成** | — |
| ~~**P0**~~ | ~~规则加载与持久化~~ | ✅ **已完成**`AgentConfig` 新增 `permission_deny_rules` / `permission_allow_rules` / `permission_ask_rules` / `permission_mode` 字段,通过 `AGENT_PERMISSIONS_*` 环境变量加载 | — |
| ~~**P0**~~ | ~~AskUser 权限交互流~~ | ✅ **已完成**executor Phase 2.5 AskUser 分支重写为完整 oneshot → SSE → 120s 超时流程。前端 `PermissionRequestCard` 组件提供 Allow / Deny / Always Allow | — |
| ~~**P0**~~ | ~~内容级权限匹配~~ | ✅ **已完成**`check(tool_name, tool_args)` 签名,`matches()` 支持 `"tool(content*)"` 格式(前缀/后缀/包含),自动提取 args 字段 | — |
| ~~**P1**~~ | ~~权限模式系统~~ | ✅ **已完成**`PermissionMode` (Default/AcceptEdits/Bypass/DontAsk)`apply_mode()` 方法,`AGENT_PERMISSION_MODE` 配置 | — |
| ~~**P1**~~ | ~~Bash 权限集成~~ | ✅ **已完成**`RunBashTool::check_permissions()` 调用 `bash_needs_permission()`安全命令自动允许executor 合并工具级检查 | — |
| ~~**P1**~~ | ~~权限 Hook 集成~~ | ✅ **已完成**`PreToolUseResult::is_permission_required()`,修复 Continue 覆盖 bugexecutor 触发 AskUser | — |
| ~~**P1**~~ | ~~子代理完整权限继承~~ | ✅ **已完成**`is_denied()` → `check(tool_name, Some(&final_args))`,子代理中 AskUser 自动拒绝 | — |
| ~~**MEDIUM**~~ | ~~拒绝追踪与熔断~~ | ✅ **已完成** | `DenialTracker`:连续/总计拒绝计数,阈值触发 ReAct 循环终止 |
| ~~**MEDIUM**~~ | ~~会话内规则更新 API~~ | ✅ **已完成** | `POST/PUT /api/chat/sessions/:id/permissions/*` 动态 add/remove/mode |
| ~~**MEDIUM**~~ | ~~权限解释器~~ | ✅ **已完成** | 启发式 `explain_permission()`Bash 风险等级 + 路径检测,随 SSE PermissionRequest 推送前端 |
| **MEDIUM** | Auto Mode (AI 分类器) | 未实现 | LLM 评估风险,快速路径 + 安全工具白名单 |
| **LOW** | 子代理最小权限 | 继承全部父工具 | `ToolRegistry::restrict()` |
| **LOW** | 文件写入大小限制 | 无上限 | 添加 `max_file_size` 参数 |
| **LOW** | 网络访问控制 | `curl`/`wget` 未限制 | Bash 黑名单扩展 |
| **LOW** | 用户权限 profiles | 不支持 | YAML/TOML 权限配置 |