dev-manager-tauri/.blueprint/modules/project-resource-broker.md
lanrtop c2b4c30f50
All checks were successful
Push & PR Check / check (push) Successful in 3m13s
Push & PR Check / check (pull_request) Successful in 3m4s
docs(resource/broker): 记录 R11 客户端试点验收
真实 Codex CLI 经临时 scoped MCP 完成非秘密 readiness、跨项目拒绝与模拟 PR;明确审批门槛及未验证边界。

Rules-Applied: R02,R03
2026-09-26 09:44:29 +09:00

17 KiB
Raw Permalink Blame History

id name status progress
project-resource-broker 项目资源与凭证代办 done 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 阻塞次数、人工取密次数、失败定位耗时的实测结果决定。

任务拆解

✅ 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。