# 7-28 [done][bug] markdown_edit 未强制 selection 允许块边界

> 发现时间：2026-05-18
>
> 状态：`[done]`
>
> 关联主线：`07-ai`

## 1. 问题定义

页面 AI context 已经可以携带 `allowedTargetBlockIds`，用于表达 `scope=selection` 时本次 run 允许修改的块集合。但 `mnote.doc.markdown_edit` 当前只按文本 search/replace 写回，不检查最终变化的 block 是否落在允许集合内。

如果 fast workflow 或未来调用者传入了过宽的 `pageText`，模型返回了选区外文本的 search/replace，服务端仍可能写入选区外块。

## 2. 证据

- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) fast workflow 调用 `mnote.doc.markdown_edit`。
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:299) 读取了 `allowedTargetBlockIds`，但未传给写工具或参与校验。
- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2860) 工具指导要求 `scope=selection` 时只能修改 `allowedTargetBlockIds` 内的块。
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1220) `markdown_edit` 生成 `changedBlocks` 后直接 dry-run 或写回，没有 selection 范围校验。

## 3. 影响

- 页面 AI 对选中文本执行编辑时，服务端缺少最终兜底边界。
- 上下文构造或模型输出一旦漂移，选区外块可能被写入。
- 与 `mnote.block.*` 已有 `allowedTargetBlockIds` / blockId 前置条件相比，`markdown_edit` 的写入安全边界较弱。

## 4. 修复

- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) fast workflow 将 `aiContext.allowedTargetBlockIds` 透传给 `mnote.doc.markdown_edit`。
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:855) `mnote.doc.markdown_edit` 在生成 `changedBlocks` 后，如果存在 `allowedTargetBlockIds` / `selectedBlockIds`：
  - 任何带 `blockId` 的变更必须属于允许集合。
  - 无 `blockId` 的 insert/full_content 变更在 selection scope 下拒绝，避免无法归属的选区外写入。
- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:2308) 增加 route 测试：`allowedTargetBlockIds=["p_2"]` 时替换 `p_1` 必须返回 `mnote_markdown_edit_target_out_of_scope`。
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:742) 增加 fast workflow 测试：模型输出修改 `p_1`，而 selection 只允许 `p_2`，必须被服务端拒绝。

## 5. 验证

```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit_rejects_selection_out_of_scope -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web block_edit_workflow -- --nocapture
cargo fmt --manifest-path rust/Cargo.toml --all -- --check
```

结果：

- `hermes_tools_markdown_edit_rejects_selection_out_of_scope`：`1 passed`。
- `block_edit_workflow_forwards_allowed_target_blocks_to_markdown_edit`：`1 passed`。
- Rust workspace format check 通过。
