重构并拆分项目

This commit is contained in:
2026-06-25 17:23:11 +08:00
parent 3476d8cbcb
commit 79ec21bfd4
127 changed files with 1555 additions and 12411 deletions
-186
View File
@@ -1,186 +0,0 @@
# 两层安全边界设计
> 审批决定"能不能做"Docker 限制"做坏了能坏到哪里",审计回答"谁让系统新增或修改了什么能力"。
## 架构
```
LLM 调用工具
→ registry.dispatch(name, params, user_id, approved)
→ load spec + validate params
→ 【授权边界】if high_risk && !approved: 统一审批(不依赖消息类型)
→ 【隔离边界】按 sandbox.profile 选择执行方式:
- local : 本地 spawnenv_clear + 白名单注入)
- stdio : docker run ias-stdio-runner (无网络、只读根)
- network : docker run ias-network-runner(联网、资源限制)
- builder : docker run ias-tool-builder (离线编译,仅 tool_manager
→ stdin 传 envelope → stdout 读 JSON → 超时 kill → 容器销毁
```
## 第一层:统一工具级审批(授权边界)
**位置**`src/tools/executor.rs` `execute_loop()` 入口
高风险工具在 **spawn 之前** 必须获得用户确认,与工具后续输出的消息类型(`result`/`http`/`db`)无关。
```rust
if spec.is_high_risk() && !approved {
// 发送确认码 → 等待用户回复 → Approved/Rejected/Expired
}
```
**修复的漏洞**:原实现审批只在 `http`/`db` 消息分支触发,`stdio → result` 可绕过。`tool_manager`high risk)直接输出 result 即可零确认执行。
## 第二层:长期沙箱容器 + docker exec(隔离边界)
### 适用范围(按威胁模型精确化)
http/db 类工具的子进程是"瘦壳"——危险操作(联网、DB 读写)全在受信任的主进程里,
给它们套容器是纯开销,**没有隔离收益**。因此:
| 场景 | 是否容器隔离 | 原因 |
|------|------------|------|
| 开发者预写工具(weather/amap/... | ❌ 本地执行 | 子进程纯计算,危险操作在主进程 |
| tool_manager 产出的工具 | ✅ stdio 容器 | 含 LLM 提供的 rust_code,不受信任 |
| tool_manager 编译 | ✅ builder 容器 | cargo 执行任意 build script/proc macro |
### 统一产物目录
所有工具二进制统一收集到 `tools/bin/``build.sh` 编译后复制,`tool_manager` 编译后也复制)。
registry 解析路径时优先用 `tools/bin/<tool>`
### 长期容器模式
daemon 启动时创建常驻容器 `ias-sandbox``tools/bin/` 只读挂载到 `/tools`
```
docker run -d --rm --name ias-sandbox \
--network none --cap-drop ALL --security-opt no-new-privileges \
--read-only --memory 512m --cpus 1 --pids-limit 128 \
--mount type=bind,source=tools/bin,target=/tools,readonly \
--mount type=tmpfs,target=/tmp \
ias-stdio-runner sleep infinity
```
工具调用通过 `docker exec` 在容器内执行(省去每次 `docker run` 的创建开销):
```
docker exec -i -e KEY=VAL ias-sandbox timeout -k 5 <secs> /tools/<name> --command <cmd>
```
- 容器已运行则复用,daemon 重启不重建
- `timeout -k 5` 超时 SIGTERM、5s 后 SIGKILL,避免孤儿进程
- env 按 spec 白名单 `-e` 注入(容器启动时仅有 PATH
- 新工具由 tool_manager 编译后复制到 `tools/bin/`,bind mount 自动可见,无需重启容器
### 两类基础镜像
| 镜像 | 用途 | 网络 | 文件系统 |
|------|------|------|----------|
| `ias-stdio-runner` | tool_manager 产出的不受信任工具(长期容器) | none | 只读根 + tmpfs /tmp |
| `ias-tool-builder` | tool_manager 编译 Rust 工具(一次性容器) | none | 可写 /workbind mount |
构建:`docker/build-images.sh`
### 容器安全基线
所有容器统一:
- `--rm` 一次性,用完销毁
- `--cap-drop ALL` 丢弃所有 Linux capability
- `--security-opt no-new-privileges` 禁止提权
- `--memory` / `--cpus` / `--pids-limit` 资源限制
- 只挂载工具二进制(readonly+ tmpfs /tmp
- 不挂载项目根目录、不挂载 docker.sock
- env 按白名单注入(`env_clear` + spec.env
### 沙箱配置(spec
```yaml
sandbox:
profile: stdio # local | stdio | builder
network: none # none | default
memory: 256m
cpus: "0.5"
pids_limit: 64
read_only: true
```
默认 `profile: local`(不启用容器)。开发者预写工具保持默认。
**tool_manager 产出的工具自动写入 `sandbox: profile: stdio`**(见 tool_manager `spec_yaml`),
不受信任代码默认进容器。
### 启用沙箱
```bash
export IAS_DOCKER_SANDBOX=1 # 工具执行走长期容器 docker exec
export IAS_DOCKER_BUILDER=1 # tool_manager 编译走 builder 容器
```
未设置时退回本地执行(仍保留 env_clear + 白名单注入)。
daemon 启动时自动 ensure `ias-sandbox` 容器,失败则回退本地执行。
### builder 容器特殊处理
`--cap-drop ALL` 丢掉 `CAP_DAC_OVERRIDE`root 无法写 bind mount 的非 root 目录。
解法:builder 容器以宿主 `uid:gid` 运行(`--user`),自然能写自己的目录。
镜像内 `CARGO_HOME=/tmp/cargo` 预缓存 serde_json 并 `chmod 777`,任意 uid 可用。
编译加 `--offline`,确保 `--network none` 下不尝试联网。
## tool_manager 加固
| 措施 | 实现 |
|------|------|
| 禁止自改 | `RESERVED_NAMES` 包含 `tool_manager`,禁止 create/update/build |
| 强制 high risk | `spec_yaml` 忽略调用方 risk_level,一律写 `high` |
| 审计日志 | 每次调用追加 `tools/.tool_audit.log`ts/uid/action/tool/result |
| 构建超时 | `timeout 180s` 包裹 cargo build |
| 失败清理 | 构建失败删除残留旧二进制,避免 reload 后调用旧版本"假成功" |
| 离线编译 | `IAS_DOCKER_BUILDER=1` 时在 builder 容器内 `--offline` 编译 |
## 结构化结果
`executor` 返回 `ToolRunOutcome` 枚举替代裸字符串:
```rust
enum ToolRunOutcome {
Ok(String),
Err { kind: ToolErrorKind, message: String },
}
```
`daemon` 据此判断是否 reloadtool_manager 成功后重载注册表),
替代原 `is_tool_error()` 的中文前缀字符串匹配。
## 审批并发修复
`ApprovalManager::clean_expired()` 两阶段加锁:
收集过期 `code_hash` → 更新 DB → 重新加锁按 `code_hash` 定位删除。
不依赖快照 index,避免并发回复导致 index 错位误删未过期审批。
## 验证
| 场景 | 结果 |
|------|------|
| tool_manager update 自身 | ❌ 拒绝(保留名) |
| create_tool risk_level=low | spec 强制 high |
| tool_manager 产出工具 spec | 自动含 `sandbox: profile: stdio` + 产物复制到 `tools/bin/` |
| datetime 本地执行 | +08:00(宿主时区,开发者工具默认不套容器) |
| tool_manager 产出工具 docker exec | 容器内 hostname=容器ID,证明确实走长期容器 exec |
| 容器常驻 | docker ps 可见 ias-sandboxexec 完无孤儿进程 |
| builder 容器编译 serde_json | ✅ 离线成功 |
| cargo test | 25 passed |
## 实施状态
| # | 步骤 | 状态 |
|---|------|------|
| 1 | 统一工具级审批 | ✅ |
| 2 | 禁止 tool_manager 自改 | ✅ |
| 3 | 强制生成工具 high risk | ✅ |
| 4 | env_clear 环境白名单 | ✅ |
| 5 | 修 clean_expired 并发 | ✅ |
| 6 | 三类 Docker 镜像 + sandbox spec | ✅ |
| 7 | stdio 工具迁移到一次性容器 | ✅ |
| 8 | network 工具迁移 | ➖ 不需要:工具走宿主代理联网,子进程纯计算,套容器无隔离收益 |
| 9 | tool_manager build 迁移到 builder 容器 | ✅ |
| 10 | 结构化结果替换 is_tool_error | ✅ |
-115
View File
@@ -1,115 +0,0 @@
# iAs 工具平台化改造 —— 当前状态
> 最后更新:2024-06-18
---
## 一、架构
```
主进程(单 tokio 进程)
├── 消息队列 (三消费者: LLM / Tool / Send)
├── 工具注册器 (扫描 tools/*/specs/*.tool.yaml)
├── 运行时 (executor::execute_loop: spawn → stdout → http/db/result)
├── 审批 (ApprovalManager, oneshot 挂起)
└── LLM (DeepSeek, 原生 tool_call, thinking 模式)
工具层(独立进程,stdin→stdout,无网络依赖)
├── tools/datetime/specs/ (1 工具, 470 KB)
├── tools/weather/specs/ (3 工具, 437 KB)
├── tools/amap/specs/ (6 工具, 441 KB)
├── tools/memories/specs/ (5 工具, 437 KB)
├── tools/web_search/specs/ (1 工具, 433 KB)
└── tools/fetch_page/specs/ (1 工具, 435 KB)
共 17 个工具, 总计 2.7 MB
```
---
## 二、LLM 调用协议
使用 DeepSeek 原生 `tool_call` (function calling),非代码块。
**2 个元工具注册给 API**
```json
[
{"function": {"name": "query_capabilities", "description": "查询所有可用工具"}},
{"function": {"name": "call_capability", "description": "调用工具", "parameters": {"name": "工具名", "prompt": "JSON参数"}}}
]
```
LLM 先调 `query_capabilities` 获取工具列表,再 `call_capability` 调具体工具。工具结果以 `role=tool` 插入上下文。
---
## 三、主进程↔工具协议
工具为短生命周期进程。每次 spawn 读 stdin 一行 JSON,写 stdout 一行 JSON。
**stdin 信封:**
```json
{"params": {"location": "北京"}, "approved": true, "user_id": "wxid"}
```
后继调用追加 `response``db_response` 字段。
**stdout 消息:**
| type | 含义 | 主进程行为 |
|------|------|-----------|
| `{"type":"http","method":"GET","url":"...","desc":"..."}` | HTTP 请求 | 审批→执行→re-spawn |
| `{"type":"db","operation":"read_memories","params":{...}}` | DB 请求 | 执行 DB→re-spawn |
| `{"type":"result","content":"..."}` | 最终结果 | 提取 content |
---
## 四、工具规范 (YAML)
每个工具目录下 `specs/*.tool.yaml`16 个字段:
```yaml
name: weather_now
desc: 查询指定城市的实时天气(温度、体感温度、风向风力、湿度、气压、能见度)
type: http
tool: weather
command: now
path: /target/release/weather
risk_level: low
timeout_secs: 10
env: [QWEATHER_KEY]
params:
- name: location
required: true
desc: 城市名称,如"北京"、"上海"、"合肥"
```
---
## 五、日志
3 路文件日志 + 终端运维日志:
| target | 文件 | 内容 |
|--------|------|------|
| `ias::msg` | msg.log | [用户] / [LLM] / [tool] |
| `ias::raw` | raw.log | [llm-request] / [llm-response] |
| `ias::err` | error.log | [spawn] [http] [timeout] [llm-chat] ... |
---
## 六、已清理
| 删除 | 原因 |
|------|------|
| `src/tools/builtins/` (8 文件) | 迁移为独立工具 |
| `src/tools/api_tool.rs` | 合并到 executor |
| `src/tools/tool.rs` | Tool trait 不再需要 |
| `src/tools/builtin.rs` | 兼容层 |
| `src/tools/shell_tool.rs` | 不需要 |
| `src/tools/stdio_tool.rs` | 合并到 executor |
| `capability-lib/` | 工具不再需要底座 |
| `tools/dist/` `tools/rules/` | 改为 tools/*/specs/ |
| `cli.rs` ToolCommand 等枚举 | 改为动态 CLI |
| `conversation.rs` 代码块解析 | 改为原生 tool_call |
-96
View File
@@ -1,96 +0,0 @@
# 实现细节
> 2024-06-18 当前状态
---
## 关键文件清单
```
src/
├── daemon.rs → 主控 (LLM/Tool/Send Consumer, Registry, execute_builtin)
├── main.rs → CLI (cmd_tool_dynamic, cmd_tools)
├── cli.rs → clap 命令定义
├── logger.rs → 三路日志初始化
├── tools/
│ ├── mod.rs → 模块声明 (6 个)
│ ├── spec.rs → ToolSpec + ParamSpec
│ ├── parser.rs → parse_dir() YAML 解析 (4 tests)
│ ├── registry.rs → Registry (load/list/dispatch)
│ ├── executor.rs → execute_loop (spawn→stdout→http/db/result)
│ └── approval.rs → ApprovalManager (6位确认码)
├── llm/
│ ├── types.rs → Message, ToolCall, StreamChunk, ConversationConfig
│ ├── conversation.rs → Conversation (run_tool_loop, summarize)
│ ├── deepseek.rs → DeepSeek provider (tool_call 流式解析)
│ └── provider.rs → LlmProvider trait
└── context/
├── types.rs → ChatSession, add_tool_result
└── builder.rs → build_context (system prompt + 摘要 + 消息)
```
---
## 工具调用全链路
```
用户消息
→ LLM Consumer: 创建 Conversation, 注册 2 元工具
→ LLM: 调 query_capabilities (tool_call)
→ ToolExecutor → PipelineMessage::ToolCall → 消息队列
→ ToolConsumer → execute_builtin → query_capabilities → 返回工具列表
→ ToolResult → LLM Consumer → resume
→ LLM: 调 call_capability(name="weather_now", prompt='{"location":"北京"}')
→ ToolConsumer → execute_builtin → call_capability 解包
→ registry.dispatch("weather_now", {"location":"北京"})
→ executor::execute_loop
→ spawn weather --command now
→ stdout: {"type":"http","url":"...","desc":"..."}
→ 审批(如需要) → reqwest::get → response
→ spawn weather --command now (带 response)
→ stdout: {"type":"result","content":"北京晴,25°C"}
→ ToolResult
→ LLM Consumer → resume → 生成回复
→ Send Consumer → 微信
```
---
## executor::execute_loop 状态机
```
spawn tool
→ stdout JSON parse
├─ {"type":"result","content":"..."} → 返回 content
├─ {"type":"http","method":"GET","url":"..."}
│ → 审批(high risk) → HTTP → 存入 response
│ → re-spawn (带 response 字段)
└─ {"type":"db","operation":"read_memories",...}
→ 审批 → MemoryStore 操作 → 存入 db_response
→ re-spawn (带 db_response 字段)
```
---
## 消息队列路由
```rust
MessageKind::UserMessage | ToolResult | ScheduledTask LLM Consumer
MessageKind::ToolCall | ApprovalRequest Tool Consumer
MessageKind::LLMReply Send Consumer
```
---
## System Prompt
```
你是 iAs 智能助手,通过微信与用户交流。回复简洁自然。
你可以通过 function calling 调用两个元工具:
1. query_capabilities — 查询所有可用能力工具
2. call_capability — 调用能力工具,参数 {"name":"工具名","prompt":"{\"参数\":\"值\"}"}
流程:收到消息 → query_capabilities → call_capability → 回复
```