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

7.9 KiB
Raw Blame History

Batch J Worker Btool availability 三处一致性审查

创建时间:2026-05-21

只读审查,不修改代码。

1. 读取过的关键文件

文件 行数 内容
rust/crates/mnote-web/src/hermes_tools/manifest.rs 400+ Tool 定义:name、description、statuscapabilityScopeannotations
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 Manifecthermes_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() 创建的条目也没有 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

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 UIlayout.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 guardhermes_tools.rs:219 + mod.rs ensure_write_authorized()

守卫有两层:

第一层hermes_tools.rs:219execute_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:186ensure_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 无动态 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.capabilitiesartifact.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. 建议的最小测试

# 已有: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,期望被拒绝