Compare commits

...
4 changed files with 14 additions and 34 deletions
@@ -123,7 +123,7 @@ xq CLI ─────┘ (Go 单二进制, 同为 MCP 客户端) ┌──
``` ```
- Claude Code:`claude mcp add --transport http xiaoqu https://mcp.<域名>/mcp --header "Authorization: Bearer <ak>"`。 - Claude Code:`claude mcp add --transport http xiaoqu https://mcp.<域名>/mcp --header "Authorization: Bearer <ak>"`。
- ak 明文只出现在本机客户端配置与签发时刻;服务端只存 SHA-256 哈希,含 ak 的客户端配置文件不得提交版本库。 - ak 明文只出现在服务端签发响应和运行时;服务端保存 SHA-256 哈希及加密密文,含 ak 的客户端配置文件不得提交版本库。
### 5.2 Web 管理后台(四个页面) ### 5.2 Web 管理后台(四个页面)
@@ -155,7 +155,7 @@ xq CLI ─────┘ (Go 单二进制, 同为 MCP 客户端) ┌──
- **`POST /api/open/token/ensure`**(get-or-create): - **`POST /api/open/token/ensure`**(get-or-create):
- 入参:`mobile`、`email`(至少一个,手机号优先)、`scopeCode`(作用域编码,必填)。 - 入参:`mobile`、`email`(至少一个,手机号优先)、`scopeCode`(作用域编码,必填)。
- 人员只认企业账号(`enterprise_type` 0/1,NULL 保留,与后台用户列表同口径);查不到 → 404。 - 人员只认企业账号(`enterprise_type` 0/1,NULL 保留,与后台用户列表同口径);查不到 → 404。
- 该人员在该作用域下**已存在**有效 Token(正常且未过期)→ `created=false`,只回 `keyPrefix`/`expireTime` 等元信息。**明文不可回吐**:服务端只存 SHA-256,这是安全红线,调用方丢了明文只能走后台重新签发。 - 该人员在该作用域下**已存在**有效 Token(正常且未过期)→ `created=false`,只回 `keyPrefix`/`expireTime` 等元信息,明文不回传。
- **不存在** → 自动创建永久 Token(一作用域一 Token,memo 标记 `open-api 自动签发`),`created=true` 返回明文,仅此一次。 - **不存在** → 自动创建永久 Token(一作用域一 Token,memo 标记 `open-api 自动签发`),`created=true` 返回明文,仅此一次。
- 全程写 `audit_log`(`tool_name=open.token.ensure`,`client_type=open-api`,参数摘要脱敏)。 - 全程写 `audit_log`(`tool_name=open.token.ensure`,`client_type=open-api`,参数摘要脱敏)。
@@ -240,7 +240,7 @@ xiaoqu-agent-service/
- 不依赖老 jar,Entity 自行生成 → 与老系统零代码依赖 - 不依赖老 jar,Entity 自行生成 → 与老系统零代码依赖
- 前后端一体打包单 jar → 可逆决定,将来可拆 - 前后端一体打包单 jar → 可逆决定,将来可拆
- 一作用域一 Token(2026-09-04)→ 收回某能力不需整把吊销;数据模型不变(token_scope 每把只写一行) - 一作用域一 Token(2026-09-04)→ 收回某能力不需整把吊销;数据模型不变(token_scope 每把只写一行)
- Open API 已存在 Token 只回元信息不回明文 → 服务端只存哈希,明文不可回吐是安全红线 - 长期 Token 仅按用户首次创建返回明文一次;后续复用元信息,不对外提供二次获取明文接口(服务端仅用于禁用/审计与治理)
- Open API 鉴权用配置文件静态密钥(dev)→ 服务间调用方少,落库+轮换留给生产阶段 - Open API 鉴权用配置文件静态密钥(dev)→ 服务间调用方少,落库+轮换留给生产阶段
- 列表类接口一律服务端分页,pageSize/rows 上限 100(超出截断为 100)→ 防止一次性拉全表,MCP 工具与后台 API 同一尺度 - 列表类接口一律服务端分页,pageSize/rows 上限 100(超出截断为 100)→ 防止一次性拉全表,MCP 工具与后台 API 同一尺度
@@ -266,4 +266,3 @@ xiaoqu-agent-service/
- 合并回 main 后用 `xiaoqu-agent-service/deploy/deploy.sh restart` 完成构建+重启,接口实测通过再 `git push`。 - 合并回 main 后用 `xiaoqu-agent-service/deploy/deploy.sh restart` 完成构建+重启,接口实测通过再 `git push`。
- 后台列表页统一服务端分页(page 从 1 开始,pageSize 默认 20、上限 100);MCP 工具 `rows` 同样上限 100,超出截断并在 `notice` 字段提示。 - 后台列表页统一服务端分页(page 从 1 开始,pageSize 默认 20、上限 100);MCP 工具 `rows` 同样上限 100,超出截断并在 `notice` 字段提示。
- **新增/修改 MCP 工具必须同步更新 `doc/agent-mcp-tools-reference.md`**(入参、出参、权限语义、示例),工具对外契约以该文档为准。 - **新增/修改 MCP 工具必须同步更新 `doc/agent-mcp-tools-reference.md`**(入参、出参、权限语义、示例),工具对外契约以该文档为准。
@@ -8,7 +8,7 @@
| 项 | 说明 | | 项 | 说明 |
|---|---| |---|---|
| 端点 | `POST http://<host>:8088/mcp`(MCP Streamable HTTP) | | 端点 | `POST http://<host>:8088/mcp`(MCP Streamable HTTP) |
| 认证 | Header `Authorization: Bearer xqak_xxx`,ak 在管理后台「用户与 Token」签发,服务端只存 SHA-256 | | 认证 | Header `Authorization: Bearer xqak_xxx`,Token 由管理后台或受信任 Open API 获取;服务端保存 SHA-256 及加密密文 |
| 工具可见性 | `tools/list` 按 ak 作用域过滤,只返回该 ak 作用域并集内且启用状态的工具(2026-09-04 起,服务端 servlet 层重写响应实现);`tools/call` 另有强制校验兜底,越权返回可读拒绝信息并落审计(denied) | | 工具可见性 | `tools/list` 按 ak 作用域过滤,只返回该 ak 作用域并集内且启用状态的工具(2026-09-04 起,服务端 servlet 层重写响应实现);`tools/call` 另有强制校验兜底,越权返回可读拒绝信息并落审计(denied) |
| 行级数据权限 | 跟 ak 所属人走,照搬老系统 `getByIsAgent` 语义(见设计文档 §3.3),与作用域无关 | | 行级数据权限 | 跟 ak 所属人走,照搬老系统 `getByIsAgent` 语义(见设计文档 §3.3),与作用域无关 |
| 分页 | 列表类工具统一 `page`(从 1 开始,默认 1)、`rows`(默认 20,**上限 100**,超出截断为 100 并在返回 `notice` 字段提示) | | 分页 | 列表类工具统一 `page`(从 1 开始,默认 1)、`rows`(默认 20,**上限 100**,超出截断为 100 并在返回 `notice` 字段提示) |
@@ -67,7 +67,7 @@
| MCP Token 过期/401 | 清除当前用户 MCP Token,重新换取一次;同一请求不自动重放工具副作用 | | MCP Token 过期/401 | 清除当前用户 MCP Token,重新换取一次;同一请求不自动重放工具副作用 |
| accessToken 刷新 | 保留 MCP Token;若服务返回 401,再重新换取 | | accessToken 刷新 | 保留 MCP Token;若服务返回 401,再重新换取 |
| 切换账号 | 先断开旧 MCP Client,再加载新账号 Token 并建立新 Client | | 切换账号 | 先断开旧 MCP Client,再加载新账号 Token 并建立新 Client |
| 退出登录 | 销毁旧 Claw/MCP Client,删除当前账号本地 MCP Token;当前永久 Token 不会自动失效,需要显式调用 revoke | | 退出登录 | 销毁旧 Claw/MCP Client,删除当前账号本地 MCP Token;当前永久 Token 以服务端状态为准,不做客户端侧远程失效调用 |
| 多账号 | Keychain key 必须包含稳定用户 ID,禁止所有账号共用 `mcpToken` | | 多账号 | Keychain key 必须包含稳定用户 ID,禁止所有账号共用 `mcpToken` |
~~建议 MCP Token 默认有效期 15 分钟,并提供换取接口;长期 AK 仅用于受控开发/管理场景。~~ ~~建议 MCP Token 默认有效期 15 分钟,并提供换取接口;长期 AK 仅用于受控开发/管理场景。~~
@@ -115,29 +115,8 @@
- 不存在则自动创建永久 Token,memo 标记 `open-api 自动签发(<scopeCode>)`。 - 不存在则自动创建永久 Token,memo 标记 `open-api 自动签发(<scopeCode>)`。
- 全程写 `audit_log`(`tool_name=open.token.ensure`、`client_type=open-api`,手机号/邮箱脱敏,不记录 Token)。 - 全程写 `audit_log`(`tool_name=open.token.ensure`、`client_type=open-api`,手机号/邮箱脱敏,不记录 Token)。
**关键约束与对策**:`created=false` 时拿不到明文。因此 xiaoqu-ai 业务后端必须在首次 `created=true` 时**自己持久化明文**(按 user_id 关联,加密存储),之后下发给本用户的客户端。
**身份约束**:业务后端不得接受客户端或模型传入的 `user_id`、`mobile`、`email` 作为身份依据。必须先从已验证的 xiaoqu-ai `accessToken` 会话解析用户,再由业务后端查询该用户的手机号/邮箱并调用 `ensure`。`X-Open-Key` 只允许服务端保管。 **身份约束**:业务后端不得接受客户端或模型传入的 `user_id`、`mobile`、`email` 作为身份依据。必须先从已验证的 xiaoqu-ai `accessToken` 会话解析用户,再由业务后端查询该用户的手机号/邮箱并调用 `ensure`。`X-Open-Key` 只允许服务端保管。
### 4.1a Token 丢失与主动失效(建议补充)
当前 `ensure` 在 `created=false` 时不会返回明文,无法解决业务后端首次落库失败或 Token 泄露后的换发问题。建议增加两个服务间接口,均使用 `X-Open-Key` 鉴权:
`POST /api/open/token/rotate`
- 按用户和 `scopeCode` 禁用当前有效 Token,创建新 Token。
- 仅本次返回新 Token 明文;旧 Token 立即失效。
- 记录旧 Token、新 Token 前缀、用户和 scope,禁止记录 Token 明文。
- 具备幂等保护,避免并发换发产生多个有效 Token。
`POST /api/open/token/revoke`
- 按用户和 `scopeCode` 禁用当前 Token。
- 不返回 Token 明文;重复调用视为成功。
- 用于退出登录后的主动失效、设备解绑和安全事件处置。
在 `rotate/revoke` 尚未实现前,退出登录只能清理客户端本地凭证,不能声称服务端 Token 已失效;管理员后台禁用仍是唯一服务端回收方式。
### 4.1b xiaoqu-ai 业务后端门面(待 xiaoqu-ai 侧实现) ### 4.1b xiaoqu-ai 业务后端门面(待 xiaoqu-ai 侧实现)
iOS 面向的接口(建议沿用原设计):POST `/api/v1/mcp/session-token`,`Authorization: Bearer <xiaoqu-access-token>`。业务后端从登录态解析用户 → 服务端查询手机号/邮箱 → 调 `ensure` → 返回 `{token, expires_at, user_id}`。不得使用请求体中的 `user_id/mobile/email` 覆盖登录态身份;客户端身份字段(client、device_id)由业务后端自行记录,agent-service 不感知。 iOS 面向的接口(建议沿用原设计):POST `/api/v1/mcp/session-token`,`Authorization: Bearer <xiaoqu-access-token>`。业务后端从登录态解析用户 → 服务端查询手机号/邮箱 → 调 `ensure` → 返回 `{token, expires_at, user_id}`。不得使用请求体中的 `user_id/mobile/email` 覆盖登录态身份;客户端身份字段(client、device_id)由业务后端自行记录,agent-service 不感知。
@@ -231,12 +210,12 @@ Token 不写入该文件。客户端运行时读取当前用户 Token,并动
| 文件/目录 | 层级 | 类型 | 改动说明 | | 文件/目录 | 层级 | 类型 | 改动说明 |
|---|---|---|---| |---|---|---|---|
| ~~`security/` JWT 验证组件~~ | Security | ~~新增~~ 取消 | agent-service 不验证 xiaoqu-ai 登录态,改由业务后端两段式调用 | | ~~`security/` JWT 验证组件~~ | Security | ~~新增~~ 取消 | agent-service 不验证 xiaoqu-ai 登录态,改由业务后端两段式调用 |
| `open/OpenTokenController.java` 等 | Open API | **已交付**(2026-09-04) | `POST /api/open/token/ensure`,X-Open-Key 鉴权,get-or-create | | `open/OpenTokenController.java` 等 | Open API | **ensure 已交付** | `ensure` 使用 X-Open-Key 鉴权 |
| `open/OpenKeyFilter.java` | Filter | **已交付** | 服务间静态密钥,常量时间比较,配置为空即关闭 | | `open/OpenKeyFilter.java` | Filter | **已交付** | 服务间静态密钥,常量时间比较,配置为空即关闭 |
| `admin/AdminTokenService.java` | Service | **已交付** | 一作用域一 Token;`issueSingle`/`findActiveToken` 供 ensure 复用 | | `admin/AdminTokenService.java` | Service | **ensure 已交付;密文保存待实现** | 一作用域一 Token;签发时保存哈希和加密密文 |
| `common/GlobalExceptionHandler.java` | Handler | **已交付** | 错误统一 `{"message": ...}` | | `common/GlobalExceptionHandler.java` | Handler | **已交付** | 错误统一 `{"message": ...}` |
| `security/AkAuthFilter.java` | Filter | 保持兼容 | `/mcp` Bearer ak 校验不变 | | `security/AkAuthFilter.java` | Filter | 保持兼容 | `/mcp` Bearer ak 校验不变 |
| `src/test/...open` | Test | **已交付** | ensure 已存在/新建/404/停用作用域/缺参 5 个用例 | | `src/test/...open` | Test | **部分已交付** | 覆盖 ensure 的参数、已存在/新建/404/停用/缺参测试 |
### xiaoqu-ai ### xiaoqu-ai
@@ -256,6 +235,7 @@ Token 不写入该文件。客户端运行时读取当前用户 Token,并动
|---|---|---:|---|---| |---|---|---:|---|---|
| 1 | ~~服务端确认 JWT 与租户映射、Token exchange 契约~~ → xiaoqu-ai 业务后端实现门面接口(登录态 → ensure) | 2h | - | 门面 401/403 语义确定;明文按 user_id 加密落库 | | 1 | ~~服务端确认 JWT 与租户映射、Token exchange 契约~~ → xiaoqu-ai 业务后端实现门面接口(登录态 → ensure) | 2h | - | 门面 401/403 语义确定;明文按 user_id 加密落库 |
| 2 | ~~服务端实现用户 MCP Token 签发与审计~~ **已交付**(ensure + 审计,2026-09-04) | - | - | 已上线并实测幂等/脱敏/错误语义 | | 2 | ~~服务端实现用户 MCP Token 签发与审计~~ **已交付**(ensure + 审计,2026-09-04) | - | - | 已上线并实测幂等/脱敏/错误语义 |
| 2b | ~~服务端增加二次获取能力~~ | - | - | 已按当前边界关闭,不再新增接口 |
| 3 | iOS Keychain 账号隔离和认证生命周期接入 | 3h | #1 | 登录/切换/退出无旧 Token 残留 | | 3 | iOS Keychain 账号隔离和认证生命周期接入 | 3h | #1 | 登录/切换/退出无旧 Token 残留 |
| 4 | Rust MCP HTTP Bearer、Session、初始化通知 | 3h | #3 | initialize/list/call 真实握手成功 | | 4 | Rust MCP HTTP Bearer、Session、初始化通知 | 3h | #3 | initialize/list/call 真实握手成功 |
| 5 | MCP 工具到 Server 路由映射 | 2h | #4 | 多 Server 下工具调用不发错目标 | | 5 | MCP 工具到 Server 路由映射 | 2h | #4 | 多 Server 下工具调用不发错目标 |
@@ -44,8 +44,9 @@ Content-Type: application/json
- `X-Open-Key` 只能配置在受信任后端,不能下发 App。 - `X-Open-Key` 只能配置在受信任后端,不能下发 App。
- 业务后端必须从已验证用户会话查询手机号/邮箱,不能信任客户端或模型传入的身份字段。 - 业务后端必须从已验证用户会话查询手机号/邮箱,不能信任客户端或模型传入的身份字段。
- `created=true` 才返回 Token 明文;业务后端必须立即加密保存。 - 首次创建返回 Token 明文;xiaoqu-mcp 同时保存 SHA-256 哈希和 KMS/独立密钥加密密文。
- `created=false` 不返回明文;Token 丢失前需先实现 rotate,退出登录需实现 revoke 才能立即失效。 - 已存在 Token 不回传明文,重复调用返回元信息并复用。
- Token 密文缺失或解密失败必须报错;不得回退到哈希值或公共 Token。
## 4. 推荐实现 ## 4. 推荐实现
@@ -73,7 +74,7 @@ Content-Type: application/json
- `src/ops_assistant_service/mcp_token_provider.py` - `src/ops_assistant_service/mcp_token_provider.py`
- 接收已验证的 `SharedIdentity`。 - 接收已验证的 `SharedIdentity`。
- 从受信任用户资料源取得 mobile/email。 - 从受信任用户资料源取得 mobile/email。
- 调用 `ensure`,首次创建时加密持久化明文。 - 调用 `ensure`,首次签发返回明文;后续直接复用本地持久化 token。
- 不接受 `user_id/mobile/email` 作为客户端覆盖参数。 - 不接受 `user_id/mobile/email` 作为客户端覆盖参数。
不引入通用 MCP 框架、第二个 Agent 或数据库直连;现有 `httpx` 已足够完成首期只读接入。 不引入通用 MCP 框架、第二个 Agent 或数据库直连;现有 `httpx` 已足够完成首期只读接入。
@@ -116,7 +117,7 @@ validate_tool_input
- 登录用户变化:先关闭旧 Session,再建立新用户 Session。 - 登录用户变化:先关闭旧 Session,再建立新用户 Session。
- MCP 返回 401:清理当前用户 Token,最多执行一次重新获取和重连;不自动重放副作用请求。 - MCP 返回 401:清理当前用户 Token,最多执行一次重新获取和重连;不自动重放副作用请求。
- 退出登录:清理本地/服务端 Token 缓存;实现 revoke 后再主动撤销远端 Token。 - 退出登录:清理本地/会话 Token 缓存;如需远端立即失效,走服务端管理端吊销能力。
- 日志只记录用户 ID、工具名、scope 和错误码,不记录 Token、Authorization Header 或手机号/邮箱明文。 - 日志只记录用户 ID、工具名、scope 和错误码,不记录 Token、Authorization Header 或手机号/邮箱明文。
- `tools/list` 仅用于校验远端能力;真正可调用集合以本地 allowlist 为准。 - `tools/list` 仅用于校验远端能力;真正可调用集合以本地 allowlist 为准。