Files
mnote/.codex/reasonix-tasks/results/batch-j-worker-b-tool-availability.md
T

183 lines
7.9 KiB
Markdown
Raw Normal View History

2026-05-21 23:53:39 +08:00
# Batch J Worker Btool availability 三处一致性审查
> 创建时间:2026-05-21
>
> 只读审查,不修改代码。
## 1. 读取过的关键文件
| 文件 | 行数 | 内容 |
|------|------|------|
| `rust/crates/mnote-web/src/hermes_tools/manifest.rs` | 400+ | Tool 定义:name、description、`status``capabilityScope``annotations` |
| `rust/crates/mnote-web/src/hermes_tools/mod.rs` | 280+ | `ToolCallInput` 结构体 + `ensure_write_authorized()` 守卫 |
| `rust/crates/mnote-web/src/routes/hermes_tools.rs` | 3691 | `execute_mnote_tool_call()` — 入口 + 工具分派 |
| `rust/crates/mnote-web/src/routes/hermes_client.rs` (L2247-2302) | ~55 | `disabled_mnote_tools()`, `mnote_tools_payload()`, `mnote_tool_entry()` |
| `rust/crates/mnote-web/src/ssr/pages/layout.rs` (L50, L6346-6390, L6895-6990) | ~100 | `pageAiTools` 状态、`pageAiNormalizeTools()`、tool 渲染 |
## 2. Manifect / UI / Execute guard 能力判断来源
### 2.1 Manifect`hermes_tools/manifest.rs`
```rust
// 每个工具定义包含:
{
"name": "mnote.doc.fetch",
"description": "读取当前页面的 canonical block projection...",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["page.read"], // 需要的权限声明
"status": "available", // 静态 availability
"annotations": tool_annotations(true, false, true, false)
// readonly, destructive, idempotent, requires_approval
}
```
关键缺陷:
- `page_get_tool()``page_save_tool()` **没有设置 `annotations` 字段**
- `available_tool()` 创建的条目也没有 `annotations`
- `status` 字段全部硬编码为 `"available"`(无动态 availability
### 2.2 Tool 列表服务端(`hermes_client.rs:2258` `mnote_tools_payload()`
```
manifest::manifest() ← 静态 manifest
disabled_mnote_tools(profile) ← 从 profile 配置 YAML 读取 mnote.tools.disabled 列表
mnote_tool_entry(tool, &disabled) ← 若 tool.name ∈ disabled,设 status=disabled, enabled=false
返回给前端
```
**disabled 来源:** `disabled_mnote_tools(profile)``hermes_client.rs:2247`
```rust
pub(crate) fn disabled_mnote_tools(profile: &str) -> Vec<String> {
let content = fs::read_to_string(profile_config_path(profile)).unwrap_or_default();
yaml_disabled_list(&content, &["mnote", "tools", "disabled"])
}
```
从 Hermes profile YAML 的 `mnote.tools.disabled` 键下读取禁用列表。
### 2.3 UI`layout.rs`
工具列表渲染链路:
```
fetch('/api/hermes/client/tools?scope=mnote&profile=' + profile)
pageAiNormalizeTools(payload)
↓ 提取: name, description, scope, status, enabled, unavailableReason
renderPageAiControls()
↓ 渲染 tool row: name + scope + status + toggle button
toggle 状态: tool.enabled !== false → aria-pressed + is-on CSS
```
UI 对所有工具平等渲染,**不隐藏 disabled 工具**(只显示 switching 开关 + `"当前 Hermes profile 已关闭该 mnote tool"` 提示)。前端不做任何独立的 capability 判断。
### 2.4 Execute guard`hermes_tools.rs:219` + `mod.rs` `ensure_write_authorized()`
守卫有两层:
**第一层**`hermes_tools.rs:219``execute_mnote_tool_call()` 入口):
```rust
if hermes_client::is_mnote_tool_disabled(&profile, &input.tool_name) {
return Err(403 "mnote_tool_disabled")
}
```
**同源**:与 listing 端调用同一个 `is_mnote_tool_disabled()``disabled_mnote_tools()`
**第二层**`mod.rs:186``ensure_write_authorized()` 在具体 tool handler 中调用):
```rust
pub fn ensure_write_authorized(context: &RequestContext, input: &ToolCallInput) -> Result<(), WebError> {
// 1. idempotencyKey 必须存在
// 2. dryRun 必须显式携带
// 3. aiAccessScope.permissionLevel 不能是只读
// 4. CommandContextBridge.ai_can_write ≠ false
// 5. CommandContextBridge.workspace_readonly ≠ true
}
```
这层不涉及 manifest 的 capabilityScope。
**第三层**`hermes_tools.rs:242` — shared read 检查):
```rust
if !dry_run && !is_read_tool(&input.tool_name) && is_shared_read_scope(&input) {
return Err(403 "mnote_tool_shared_read_write_forbidden")
}
```
通过 `aiAccessScope.permissionLevel` 判断共享只读。
## 3. 三处是否同源
### 3.1 Disabled 列表:✅ 完全同源
| 层 | 判断 | 来源 |
|----|------|------|
| Manifect | 无动态 disabledstatic | — |
| Listing | `disabled_mnote_tools(profile)` | profile YAML `mnote.tools.disabled` |
| UI | 接收 `enabled` 字段 | 来自 listing endpoint |
| Execute guard | `is_mnote_tool_disabled(profile, name)` | 同一 `disabled_mnote_tools()` |
所有层在"哪些工具被禁用"上使用同一数据源(Hermes profile YAML)。
### 3.2 capabilityScope 执行:⚠️ 不同源(P1
| 层 | capabilityScope 用法 | 角色 |
|----|---------------------|------|
| Manifect | 每个工具声明 `capabilityScope: ["page.read"]``["page.write"]` | **文档性** — 告诉 AI agent 需要什么权限 |
| Listing | 转发 `capabilityScope` 到 UI | **展示性** |
| UI | 不消费 `capabilityScope` 做判据 | **不展示权限要求** |
| Execute guard | **不检查** `input.capability_scope` 与 manifest scope 的匹配 | **未执行** |
**关键缺口**:manifect 声明了工具需要的权限范围(`page.read` / `page.write` / `block.write` 等),但 execute guard 没有验证调用方声明的 `capability_scope` 是否包含目标工具所需的 scope。
`ToolCallInput.capability_scope` 虽存在于结构体中,但只做:
1. 传递给具体 tool handler 作为 `RuntimeTargetWire.capabilities`artifact.rs:187, block.rs:1136, page.rs:279
2. 记录到 audit 日志(hermes_tools.rs:433
3.`page_ai_workflow.rs:130` 在调用时手动设置 `capability_scope: Some(vec!["block.write".into(), "page.write".into()])`
无中心守卫验证 `input.capability_scope` 与 manifest 中该 tool 的 `capabilityScope` 的包含关系。
## 4. P0/P1/P2 分级缺口
### P0:无
disabled 工具在三层间保持一致,没有安全绕过。
### P1capabilityScope 未在 execute guard 强制执行
- **漏洞描述**:Agent 可以声明 `capability_scope: ["page.read"]`,但仍然调用 `mnote.page.save`(写工具)。守卫只检查 `is_shared_read_scope`(看 `aiAccessScope.permissionLevel`)和 `is_read_tool`(看硬编码列表),不检查 manifest 声明的 scope。
- **影响**:低(不构成安全漏洞,因为还有 `aiAccessScope.permissionLevel` + 文件系统权限 + 授权 root 三层兜底),但 manifests 声明的 scope 信息完全浪费。
- **修复建议**:在 `execute_mnote_tool_call()` 中,校验 `input.capability_scope` 是否覆盖 manifest 中该工具的 `capabilityScope`
### P2Manifest 元数据不完整
| 缺陷 | 位置 | 说明 |
|------|------|------|
| `page_get_tool()``annotations` | `manifest.rs` 末尾 | 没有 `readonly`/`destructive` 标记 |
| `page_save_tool()``annotations` | `manifest.rs` 末尾 | 同上 |
| `available_tool()` 创建的工具缺 `annotations` | `manifest.rs` | `mnote.page.update_title`, `mnote.page.update_options`, `mnote.artifact.create_summary`, `mnote.artifact.create_ai_note` |
| `status` 字段全部硬编码 | `manifest.rs` | 无动态 availabilitymanifect 不含 disabled 状态(在 listing 层叠加) |
| 前端 UI 不展示 `capabilityScope` | `layout.rs:6346-6380` | `pageAiNormalizeTools` 解析但不消费,渲染时不显示权限要求 |
## 5. 建议的最小测试
```bash
# 已有:disabled tool 被 execute guard 拒绝
cargo test -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool
# 已有:write guard 检查(aiAccessScope、idempotencyKey
cargo test -p mnote-web ensure_write_authorized
# 新增建议:capability_scope 校验测试
# 位置:hermes_tools/mod.rs tests
# 内容:构造 ToolCallInput { capability_scope: ["page.read"] } 调用 mnote.page.save,期望被拒绝
```