dev-manager-tauri/.blueprint/modules/project-resource-broker.md
lanrtop 4a37438a58
All checks were successful
Push & PR Check / check (push) Successful in 1m29s
fix(credential): 修复并行测试偶发失败——Credential Manager 调用串行化,模拟 Gitea 改阻塞读
全量并行 cargo test 10 次失败 3 次(单跑稳定通过),两个独立根因:

- 同进程并发 CredWriteW/CredDeleteW 会丢更新:写入并校验成功的凭据随后读回 NOT_FOUND,
  删除成功的条目又被并发写回。R10 readiness 报 needs_repair、开发变量试点报「凭证引用缺失」
  均源于此(诊断日志证实无跨测试删除;8 线程压力实验直接复现)。windows_store 的
  put/get/delete 经进程级锁串行,生产中 MCP 与 UI 并发写凭据同样受益;跨进程不在保护范围
- Windows 上从 nonblocking listener accept 的 socket 继承 nonblocking,set_read_timeout
  不生效,请求晚到即 WouldBlock。mcp_server 两处模拟 Gitea 补 set_nonblocking(false),
  与 commands/gitea.rs 既有写法一致

验证:修复后全量并行 cargo test 连续 30 次 0 失败(247 passed、5 ignored)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:52:05 +09:00

180 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: project-resource-broker
name: 项目资源与凭证代办
status: done
progress: 100
---
## 概述
让项目 Agent 发现“某项资源是否可用”,并通过炼境调用有限、可审计的操作;Agent 不需在对话、MCP 输出、项目文件中接收原始凭证。统一的是入口与操作契约,不是把所有项目密钥集中到炼境一处。
## 设计决策:按用途存放,按操作代办(2026-09-23)
### 背景与证据
- `api_keys.api_key`、`gitea_instances.access_token`、`gitea_repos.webhook_secret` 目前是 SQLite 文本列;`get_api_keys`、`gitea_list_instances`、部分仓库 DTO 会将原值返回 Tauri 前端。`ApiKeysPanel` 的遮罩发生在前端,不能当作后端不暴露。
- 现有 MCP `create_pull_request(project_id, ...)` 已由炼境内部使用 Gitea token,调用方不需要提取 token。应以此为首个资源代办样板,不另造 PR 接口。
- 现有 `/mcp/projects/<project_id>` 只提供单机误操作隔离,并非认证或不同 OS 进程间的秘密隔离。Agent 若与用户共享 Windows 账户,不能宣称其物理上绝对读不到本机凭证。
- Enterprise System 的 production 链路保留 Gitea Actions、项目部署脚本和服务器运行环境;炼境当前只读同 SHA 交付证据,不得由本模块扩张为 SSH、数据库、migration 或部署执行者。
### 方案选择
| 方案 | 收益 | 代价 / 风险 | 结论 |
| --- | --- | --- | --- |
| 将全部 `.env`、CI、服务器凭证导入炼境,提供通用 `get_secret` | 表面上配置统一 | 单点风险、同用户 Agent 可读取、破坏 CI/运行时现有信任边界 | 否决 |
| 维持现状,仅增加“不要读取密钥”的 Skill 提醒 | 改动少 | 明文存储与原值 DTO 仍在,无法形成可验证边界 | 否决 |
| **按执行用途保留凭证原位;炼境仅托管本机代办凭证引用和最小操作** | 复用已有 PR 能力,渐进迁移,Agent 只见状态和操作结果 | 需兼容旧数据,且同用户本机威胁模型有限 | **选定** |
### 凭证归属与边界
| 用途 | 凭证权威位置 | 炼境职责 | Agent 可见 |
| --- | --- | --- | --- |
| 本机 Gitea API 代办 | Windows 用户保护的本机凭证存储;SQLite 仅保存稳定引用与非秘密元数据 | 在项目已绑定 Gitea 仓库且操作被允许时,代办现有 PR/状态调用 | `configured / unavailable / needs_repair` 与 PR 结果;无 token |
| Gitea Actions 中的 CI/部署 | Gitea Actions secrets | 只读检查 CI 与交付证据 | 状态与证据引用;无 secret |
| 项目进程的数据库、第三方服务、服务器运行时 | 项目自己的运行环境 / 云端现有机制 | 首轮只报告资源是否已配置,不接管值 | 非秘密 readiness;无连接串、账号密码 |
| 开发者手工保存的通用 API Key | 炼境本机受保护存储(通过单独迁移卡处理) | 保留人的显式展示/复制操作;不提供 MCP 原值读取 | 常态列表仅元数据;Agent 不获 reveal 工具 |
首轮不增加任意 URL/命令代理,不增加 `get_secret` / `export_env` / `run_sql` / `ssh_exec` / `deploy`。新操作的授权必须在能力本体服务层检查项目绑定、目标、动作;不能靠前端置灰或 MCP 描述约束。写操作沿用现有确认与 Git/PR 规则。读取的资源状态不得含凭证、用户名、连接串、敏感目标详情。若将来要求抵抗同一 OS 用户下的恶意进程,需另立认证与进程隔离设计,不能拿 loopback/scoped URL 充当安全边界。
### 受影响文件与数据流
- 现有读取:`src-tauri/src/commands/api_keys.rs`、`src-tauri/src/commands/gitea.rs`、`src-tauri/src/db.rs`;前端契约 `src/lib/commands.ts`,展示 `src/components/dashboard/ApiKeysPanel.tsx`、`src/components/dashboard/GiteaPanel.tsx`。
- 建议新增:`src-tauri/src/credential_store.rs`,隔离 Windows 凭证存储与可注入测试接口;实际 API/依赖在卡 R3 的技术验证后固定。Rust 模块入口需在 `src-tauri/src/lib.rs` 注册。
- 建议新增只读状态:后端项目资源 readiness 聚合函数,并由 `src-tauri/src/mcp_tools.rs` 公开项目 scoped 查询;`src/lib/commands.ts` 提供同一本体的人类投影。v1 仅 Gitea 绑定、凭证可用性等非秘密状态,不泛化所有 provider。
- 外部契约变化须同步 `docs/ai-context/project-context.yaml` 和相关调用说明。任何 SQLite schema 改动须在 `src-tauri/src/db.rs::migrate()` 中添加兼容迁移,不能更改旧安装数据的含义或静默清空。
### 迁移与失败模型
1. 先增加脱敏 DTO 与回归测试;人的 API Key 展示/复制改为显式动作,常态列表不携带原值。此步骤不删 SQLite 原值。
2. 以 Windows Credential Manager 为优先候选,在隔离测试中验证写入、读取、覆盖、删除、用户上下文、重启后读取与错误返回;若不满足要求,停止迁移并重新决策,不静默退回明文 SQLite。
3. 迁移每条旧凭证时采取“写新存储 → 读取校验 → 切换引用 → 再清除旧列”的可恢复顺序;失败保留旧值并显示 `needs_repair`,不把失败伪装为 `not_configured`。设计并演练备份/恢复后再动真实用户数据;旧数据库备份可能仍含明文,不能宣称迁移后历史明文已消失。
4. 凭证存储不可用时,代办操作 fail closed,项目自有 CI/运行链路照常;状态明确提示缺少/不可用/需修复,日志、错误文本和测试快照均不得回显原值。
### 验证路径
- 载体:Rust 纯函数及带内存数据库/模拟凭证存储的 `cargo test`;MCP 只读工具做真实 loopback/scoped 握手验证;前端展示由人终验。
- 示例:`[cargo test] gitea_list_instances()` → JSON 不含 `access_token`;`get_project_resource_status(p1)` → 仅非秘密状态,p2 参数被 scope 拒绝。
- 示例:`[cargo test] migrate_legacy_credential(模拟存储失败)` → 旧值仍可恢复、状态为 `needs_repair`、不泄露凭证。
- 示例:`[MCP] create_pull_request(p1, head, base, title)` → 返回 PR 标识或可行动错误,不返回 token;不把生产部署作为测试副作用。
### 风险与决策闸门
- **同一 Windows 用户可读性**:本方案减少凭证进入 Agent 上下文与普通 DTO 的机会,不提供强进程隔离;验收不能写成“Agent 绝对无法获取”。
- **旧用户数据迁移风险**:先用复制的匿名/模拟 DB 演练失败与回滚,再对真实数据执行;无验证、无备份方案不得迁移。
- **功能回退风险**:API Key 显示/复制保留为人的显式操作;Gitea PR 创建和 webhook 签名必须回归。
- **`Cargo.toml` 当前有既有未提交改动**:实施存储 adapter 若需改依赖,先确定改动归属并使用隔离工作区;不得混合提交或覆盖。
- **范围膨胀风险**:Enterprise 非生产样板只验证一个真实操作;是否扩展第二种资源,由减少 Agent 阻塞次数、人工取密次数、失败定位耗时的实测结果决定。
## 任务拆解
### ✅ R12 — 修复并行测试偶发失败:Credential Manager 串行化 + 模拟 Gitea 阻塞读
- status: done
- complexity: S
- files: src-tauri/src/credential_store.rs, src-tauri/src/mcp_server.rs
- depends: R3, R10
- acceptance: "agent_verify: [cargo test] 全量并行连续 30 次 → 0 失败(修复前基线 10 次失败 3 次:R10 scoped_mcp_create_pull_request_uses_synthetic_credential 的 readiness=needs_repair、scoped_dev_variable_http_pilot 的「凭证引用缺失」;仅修凭据后 30 次仍有 1 次 R10 模拟 Gitea read_line WouldBlock)。"
- verification: "2026-10-02:修复后全量并行 cargo test 连续 30 次 0 失败(247 passed、5 ignored);仅凭据串行化的中间版本 30 次中凭据类失败为 0。"
- unverified: "跨进程并发(如 tauri dev 实例与 cargo test 同时写凭据)不受进程内锁保护;历史失败可能在本机 Credential Manager 残留无权限合成条目,非 diag 前缀的未逐条清理。"
- root_cause: "①同进程并发 CredWriteW/CredDeleteW 丢更新——写入成功后立即读回 NOT_FOUND、删除成功的条目被并发写回(8 线程压力实验复现;诊断日志证实无跨测试删除);②Windows 上 nonblocking listener accept 出的 socket 继承 nonblocking,set_read_timeout 不生效,客户端请求晚到即 WouldBlock(commands/gitea.rs 已有同款修复,mcp_server 两处 mock 遗漏)。"
### ✅ R11 — 隔离真实客户端试点宿主
- status: done
- complexity: M
- files: src-tauri/src/mcp_server.rs, .blueprint/modules/project-resource-broker.md, .blueprint/manifest.yaml
- depends: R10
- acceptance: "agent_verify: [cargo test --ignored] isolated_scoped_mcp_client_pilot → 仅用内存数据库、随机合成 Windows 凭证与 127.0.0.1 模拟 Gitea 启动临时 scoped MCP;收到一笔有界 PR 请求后断言模拟端收到 token、测试退出时凭证清理。[cargo test] R10 继续断言 readiness 无明文与跨项目调用拒绝。[CLI] cargo test、git diff --check 通过。human_verify: Codex/Claude 客户端实际连接临时 scoped URL,依次调用 readiness、跨项目拒绝与模拟 PR,客户端会话只见非秘密状态和 PR 结果、不见 token;日常应用和真实数据库未被修改。"
- human_verify: "Codex CLI 独立模型会话已实际连接临时 scoped URL:readiness 仅返回 configured/not_configured,跨项目调用返回 scope_violation,审批后的模拟 PR 返回 #43;会话输出不含 token。未启动第二个日常 Tauri 实例或打开真实用户数据库。"
- verification: "2026-09-26:ignored Rust fixture 与独立协议进程三轮验收;随后 Codex CLI 真实模型会话以一次性 `-c` 覆盖连接同一合成 scoped MCP,依次调用 readiness、跨项目拒绝和模拟 PR。默认非交互 approval=never 阻止 PR;单次 `--approve-for-me` 开启审批后,mock Gitea 返回 #43 且模型结果不含 token。fixture exit 0,断言 Windows 合成凭证与临时目录清理。完整 cargo test 211 passed、2 ignored;typecheck、前端 6 tests、git diff --check 通过;R11 push CI 成功。"
- unverified: "真实用户数据库/凭证迁移、正式安装版 UI、真实 Gitea PR/生产部署均未测试;强制终止测试进程时 Drop 不保证执行,可能留下无生产权限的随机合成 Windows 凭证;同一 Windows 用户下不提供强进程隔离。"
- retrospective: "【复盘】隔离真实客户端试点宿主|任务类型: MCP工具|复杂度: M|实际轮数: 2轮(预估 2 轮)|是否返工: 是|返工原因: Codex CLI 非交互 approval=never 会拒绝写工具,即使客户端可读取 scoped readiness|有效策略: 先确认临时 URL 仅指向内存 fixture 与 mock Gitea,再以一次性审批模式完成真实模型调用,并在正常测试退出时断言清理|遗留风险: 真正的日常 UI 与真实旧数据迁移仍未验证;强制终止时合成 WinCred 可能残留|auto_applied_rules: R02,R03|rule_effectiveness: R02=有效,R03=有效"
### ✅ R10 — scoped MCP 合成 PR 端到端回归
- status: done
- complexity: M
- files: src-tauri/src/mcp_server.rs
- depends: R4, R7, R8
- acceptance: "agent_verify: [cargo test] scoped_mcp_create_pull_request_uses_synthetic_credential → 真实 127.0.0.1 MCP initialize/tools/list/tools/call 经项目作用域到模拟 Gitea;请求使用合成受保护凭据,结果仅含 PR 标识、不含 token,跨项目参数拒绝;测试后合成凭据引用被清理。[CLI] cargo test、git diff --check 通过。human_verify: 无(仅合成测试,不接触真实仓库)。"
- retrospective: "【复盘】scoped MCP 合成 PR 端到端回归|任务类型: MCP工具|复杂度: M|实际轮数: 2轮(预估 2 轮)|是否返工: 是|返工原因: 初版模拟服务无接入超时且清理守卫建立偏晚|有效策略: 内存数据库 + 随机 Windows 凭据引用 + 127.0.0.1 mock,复用真实 MCP 路由并在 Drop 中清理|遗留风险: 该串联测试仅在 Windows 执行;真实 Gitea 试点仍需单独授权|auto_applied_rules: R13|rule_effectiveness: R13=有效"
### ✅ R9 — PR 审查修复:缓存失效、凭证删除与 readiness
- status: done
- complexity: M
- files: src/components/dashboard/ApiKeysPanel.tsx, src/components/dashboard/ApiKeysPanel.test.ts, src-tauri/src/credential_store.rs, src-tauri/src/commands/api_keys.rs, src-tauri/src/commands/gitea.rs, src/lib/commands.ts
- acceptance: "agent_verify: [cargo test] 凭证删除失败可恢复且不会假成功,legacy readiness 为 pending_migration 而非 configured;[vitest] copyCurrentApiKey 编辑后复制最新值;[CLI] typecheck、测试与 git diff --check 通过。human_verify: 正式安装版编辑后显示/复制终验。"
- human_verify: pending(正式安装版 UI 未在本轮重跑;agent 路径已通过)
### ✅ R1 — Gitea 与 webhook 输出脱敏
- status: done
- complexity: M
- files: src-tauri/src/commands/gitea.rs, src/lib/commands.ts
- depends: gitea-integration
- acceptance: "agent_verify: [cargo test] gitea_list_instances / gitea_repo DTO 序列化 → 不含 access_token 或 webhook_secret;原有 Gitea 连接测试仍通过。human_verify: 设置页能显示实例并执行改 Token,不在常态 UI 或调试响应显示原值。"
- human_verify: pending(需用户在隔离功能交付后终验真实 UI;agent 验证已通过)
### ✅ R2 — API Key 常态列表脱敏与人的显式查看
- status: done
- complexity: M
- files: src-tauri/src/commands/api_keys.rs, src/lib/commands.ts, src/components/dashboard/ApiKeysPanel.tsx
- depends: R1
- acceptance: "agent_verify: [cargo test] get_api_keys() → 列表序列化不含 api_key,显式 reveal 按不存在 id 返回错误,MCP tools/list 无 reveal 工具。human_verify: 人主动点击后仍可查看/复制,默认列表不显示原值。"
- human_verify: pending(需用户终验显式查看/复制;agent 验证已通过)
### ✅ R3 — Windows 本机凭证存储可行性与适配接口
- status: done
- complexity: M
- files: src-tauri/src/credential_store.rs, src-tauri/src/lib.rs
- depends: R2
- acceptance: "[cargo test] 凭证存储适配器在模拟存储上完成 put/get/replace/delete 与失败注入;Windows 隔离测试重启后按引用读取同一测试值,失败时不回显值;不满足则停止后续迁移并记录证据。"
### ✅ R4 — Gitea 凭证兼容迁移与代办回归
- status: done
- complexity: M
- files: src-tauri/src/db.rs, src-tauri/src/commands/gitea.rs, src-tauri/src/commands/git_ops.rs, src-tauri/src/credential_store.rs
- depends: R1, R3
- acceptance: "[cargo test] 旧 Gitea token 在模拟数据库中 write→verify→switch→clear;注入存储失败时旧值可恢复且状态 needs_repair;create_pull_request 与 webhook 注册仍使用有效凭证,响应不含 token。"
### ✅ R5 — webhook 签名凭证兼容迁移
- status: done
- complexity: M
- files: src-tauri/src/db.rs, src-tauri/src/commands/gitea.rs, src-tauri/src/mcp_webhook.rs, src-tauri/src/credential_store.rs
- depends: R1, R3, R4
- acceptance: "[cargo test] 已登记 webhook_secret 的旧仓库经 write→verify→switch→clear 后仍可验证 webhook 签名;模拟存储失败时旧值可恢复,仓库 DTO 与日志均不含 secret。"
### ✅ R6 — API Key 凭证兼容迁移与显式查看回归
- status: done
- complexity: M
- files: src-tauri/src/db.rs, src-tauri/src/commands/api_keys.rs, src-tauri/src/credential_store.rs, src/components/dashboard/ApiKeysPanel.tsx, src/lib/commands.ts
- depends: R2, R3
- acceptance: "[cargo test] 旧 API Key 迁移后列表仍无原值,人的显式 reveal 返回原值;模拟迁移失败保留可恢复数据且不产生半迁移的假成功。"
### ✅ R7 — 项目资源只读 readiness 与操作边界
- status: done
- complexity: M
- files: src-tauri/src/commands/gitea.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/mcp_server.rs, src-tauri/src/lib.rs, src/lib/commands.ts
- depends: R4, R5
- acceptance: "[cargo test] get_project_resource_status(p1) → configured/unavailable/needs_repair 有来源且不含凭证;p2 越界被拒绝;MCP tools/list 无 get_secret、任意 HTTP/SSH/SQL 代理。"
### ✅ R8 — 契约文档与 Enterprise 非生产样板终验
- status: done
- complexity: M
- files: docs/ai-context/project-context.yaml, docs/ai-context/agent-button-system.md, docs/ai-context/project-brief.md, AGENTS.md, .blueprint/manifest.yaml, .blueprint/modules/project-resource-broker.md
- depends: R6, R7
- acceptance: "agent_verify: [MCP] Enterprise 项目调用只读 resource status → 返回非秘密 readiness;[cargo test] 模拟 Gitea 调用 create_pull_request → 仅返回 PR 标识/可行动错误,日志与调用结果无原始凭证;[CLI] git diff --check → exit 0。human_verify: 确认 Gitea/CI/服务器凭证仍在原用途位置,未创建真实 PR、触发生产部署或数据库写入。"
- human_verify: pending(真实 UI 与凭证归属留待用户终验;agent 非生产路径已通过)
## 实施验证(合成数据,未接触真实生产密钥)
- R1–R2:DTO/API Key 常态列表序列化测试通过;人的显式 API Key reveal 只在本机 Tauri command,MCP 不暴露。
- R3:Windows Credential Manager 随机测试引用跨进程读取并清理;模拟存储的写、读、覆盖、删除与失败注入通过。
- R4–R6:旧值迁移、存储/数据库失败保留旧值、SQLite 迁移前备份及恢复演练、新建/更新/删除回归通过;模拟 Gitea PR 与 webhook 注册均只访问 `127.0.0.1`。
- R7–R8:内存 Enterprise fixture 经真实 loopback MCP HTTP/SSE 完成 `initialize → tools/list → tools/call`,readiness 不含凭证,跨项目参数被拒绝。
- R10:Windows 合成项目经真实 scoped MCP `initialize → tools/list → tools/call(create_pull_request)` 到 `127.0.0.1` 模拟 Gitea;代办内部使用受保护存储的合成 token,Agent 结果仅含 PR 信息,跨项目参数被拒绝,测试后凭据引用清理;全量 Rust 测试通过(211 passed、1 ignored)。
- 未验证:真实用户 DB/凭证迁移、真实 Gitea/CI/服务器、正式安装版 UI 人工终验和真实 PR/部署;现有备份若不含当前旧值会拒绝迁移并要求人工安排新备份,不会降级明文。
## 阶段退出标准
- P0(R1–R2):普通列表/DTO 不再携带原始值;不误称数据库已加密。
- P1(R3–R6):本机代办凭证的受保护存储与兼容迁移可恢复,现有 Gitea/API Key/webhook 能力不回退。
- P2(R7–R8):Agent 能获取 Enterprise 的非秘密 readiness,PR 代办通过模拟 Gitea 验证;真实 PR 试用需另行按交付流程授权。量化是否真减少人工传密和失败定位时间;未达到收益门槛,不扩成通用 Vault。