7.9 KiB
Batch J Worker B:tool 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)
// 每个工具定义包含:
{
"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()创建的条目也没有annotationsstatus字段全部硬编码为"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)
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() 入口):
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 中调用):
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 检查):
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 | 无动态 disabled(static) | — |
| 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 虽存在于结构体中,但只做:
- 传递给具体 tool handler 作为
RuntimeTargetWire.capabilities(artifact.rs:187, block.rs:1136, page.rs:279) - 记录到 audit 日志(hermes_tools.rs:433)
- 由
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 工具在三层间保持一致,没有安全绕过。
P1:capabilityScope 未在 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。
P2:Manifest 元数据不完整
| 缺陷 | 位置 | 说明 |
|---|---|---|
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 |
无动态 availability;manifect 不含 disabled 状态(在 listing 层叠加) |
前端 UI 不展示 capabilityScope |
layout.rs:6346-6380 |
pageAiNormalizeTools 解析但不消费,渲染时不显示权限要求 |
5. 建议的最小测试
# 已有: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,期望被拒绝