# 7-46 ONLYOFFICE live tool 缺少 resource scope 绑定

## 状态

- 状态：done
- Owner：07-ai / Hermes tools / OnlyOffice live bridge
- 发现时间：2026-05-31

## 现象

ONLYOFFICE live 写工具当前主要检查 `idempotencyKey`、`dryRun`、`aiAccessScope.permissionLevel` 和 command context 写权限，但没有把 bridge session 的 `documentId` / `assetId` 与 `aiAccessScope.allowedResourceIds` 绑定。调用方如果显式传入另一个已注册 Office session，存在越过当前 target resource scope 写入非授权资源的风险。

## 证据

- `rust/crates/mnote-web/src/routes/hermes_tools.rs` 注册了多组 `mnote.onlyoffice.*` 读写工具。
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs` 的 `run_write_action()` 调用 `ensure_write_authorized()` 后立即解析 session 并执行 bridge command。
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs` 的 `resolve_session_id()` 只解析显式 session 或全局 current session，没有验证该 session 对应的 resource 是否在 `allowedResourceIds` 内。
- 对照 `rust/crates/mnote-web/src/hermes_tools/resource.rs`，mindmap/resource 工具已有 `ensure_resource_scope_allowed()` 校验 `allowedResourceIds` / `objectIdentity`。

## 影响

- Page AI 当前 target 是资源 A 时，模型或恶意调用可传入资源 B 的 `bridgeSessionId`，尝试写入 B。
- 写入审计会显示工具有写权限，但缺少“写的是哪个 resource、是否被 allowedResourceIds 授权”的闭环。
- 多 tab session fallback 与本 bug 叠加时，错误写入更难被用户发现。

## 最小复现建议

1. 构造 `aiAccessScope.allowedResourceIds = [A]`。
2. 注册两个 ONLYOFFICE bridge session：A 和 B。
3. 调用 `mnote.onlyoffice.sheet.set_value` 或 `mnote.onlyoffice.document.insert_text`，显式传 B 的 `bridgeSessionId`。
4. 期望：返回 403；当前风险：只要通用写权限通过就可能执行。

## 修复建议

- `BridgeSessionInfo` 暴露稳定 `documentId`、`assetId`、`objectIdentity`。
- ONLYOFFICE live 工具增加与 resource 工具等价的 scope 校验。
- 写工具返回 receipt 时包含 `resourceKind=office`、`documentId`、`assetId`、`onlyofficeSessionId` 与 permission decision。
- dry-run 也必须执行 scope 校验，不能只返回 wouldWrite。

## 本轮进展

- 2026-05-31：
  - `onlyoffice_live.rs` 已在读/写 action 执行前校验 explicit session 对应的 `sessionId` / `documentId` / `assetId` / `resource:office:{documentId}:{assetId}` 是否包含在 `aiAccessScope.allowedResourceIds`。
  - 缺失 `aiAccessScope` 或空 `allowedResourceIds` 现在返回 403，不再兼容放行；dry-run 同样执行该 scope 校验。
  - `manifest.rs` 已为 OnlyOffice live 工具声明 `aiAccessScope.allowedResourceIds` 和 `onlyofficeSessionId` / `bridgeSessionId` 的 `anyOf` 合同。
  - 已补 Rust 定点测试覆盖 `allowedResourceIds=[asset_a]` 时禁止写入 session `asset_b`，`allowedResourceIds=[asset_allowed]` 时允许生成 dry-run plan，缺 scope 时返回 403，以及 manifest 合同。
  - 已补并通过 `scripts/task515-onlyoffice-live-scope-http-smoke.js`：HTTP 层覆盖缺 explicit session 400、scope 不匹配 403、缺 scope 403、授权 scope dry-run 200。
- 2026-06-01：
  - resource scope candidate 已同时接受 `resource:office:{documentId}:{assetId}` 与 FileTree / resource 对象侧使用的 `resource:onlyoffice:{documentId}:{assetId}`，避免真实 Page AI target 使用 OnlyOffice object identity 时被误拒。
  - 已补并通过 `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js`：真实 ONLYOFFICE iframe / DocumentServer 下，显式传入 Office B 的 `bridgeSessionId` 但 `aiAccessScope.allowedResourceIds=[Office A]` 时返回 403；授权 B 的 `assetId` / `resource:onlyoffice:{documentId}:{assetId}` 时 dry-run 返回 200。
  - `document-resource-tab-runtime.js` / `sidebar-page-ai-runtime.js` / `hermes_client.rs` 已补最小 target scope 链路：Office resource target 可把 `onlyofficeSessionId` 写入 target package，服务端 sanitize 不再丢弃 `primaryTargetId`、`targets[]`、`assetId`、`onlyofficeSessionId`，本地 run instructions 的 `aiAccessScope.allowedResourceIds` 会包含 Office asset、session 和 `resource:onlyoffice:{documentId}:{assetId}`。新增 `hermes_client_run_body_preserves_onlyoffice_target_scope` 证明服务端不再把 Office target scope 降级为当前页面 id。
  - 本地 agent instructions 已明确要求从 `agentRunEnvelope.targetPackage.onlyofficeSessionId` 或对应 target 取值传给 `mnote.onlyoffice.*` 工具，不允许 fallback 到最近活跃 Office session。
  - `task518` 已扩展并通过授权 B session 的非 dry-run 写入验证：写入后 `document.export` 证明 B 包含唯一 marker，A 不包含，补齐真实 iframe 层写入落点证据。
  - 已补并通过 `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`：真实 Page AI UI 选择 Office resource target 后，run payload 中 `editorTarget.onlyofficeSessionId`、`targetPackage.onlyofficeSessionId` 和 `targetPackage.targets[0].onlyofficeSessionId` 均等于 iframe live bridge session；`allowedFiles` 只包含选中 Office 文件路径，且无 buffer-state 404 / console error。

当前 `task518` 已覆盖真实 iframe session + 工具层 resource scope + 非 dry-run 写入落点，`task523` 补齐真实 Page AI UI target picker 到 live Office session 的 run payload 绑定；组合后覆盖“UI 只授权选中 Office target，工具层拒绝未授权 session”的端到端安全边界。

## 验收

- [x] Rust 单测覆盖 allowedResourceIds 不包含 session resource 时写工具返回 403。
- [x] Rust 单测覆盖 allowedResourceIds 包含 `assetId` 或 `objectIdentity` 时允许执行。
- [x] Rust 单测覆盖缺失 / 空 resource scope 时拒绝。
- [x] HTTP smoke 覆盖工具层 session/scope 边界。
- [x] Browser smoke 覆盖真实 ONLYOFFICE iframe session 的工具层 A/B resource scope 边界。
- [x] Rust 单测覆盖 Page AI Office target package 生成的 aiAccessScope 包含 Office asset/session/object identity。
- [x] Browser smoke 覆盖真实 Page AI UI 选择 Office target 后只把该 Office 的 live session 与 relativePath 冻结进 run payload；工具层 A/B session 越权拒写由 `task518` 覆盖。
