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

183 lines
7.9 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.
# 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,期望被拒绝
```