# Secrets MCP — AGENTS.md 本仓库为 **MCP SaaS**:`secrets-core`(业务与持久化)+ `secrets-mcp`(Streamable HTTP MCP、Web、OAuth、API Key)。对外入口见 `crates/secrets-mcp`。 ## 版本控制 本仓库使用 **[Jujutsu (jj)](https://jj-vcs.dev/)** 作为版本控制系统(纯 jj 模式,无 `.git` 目录)。 ### 常用 jj 命令对照 | 操作 | jj 命令 | |------|---------| | 查看历史 | `jj log` / `jj log 'all()'` | | 查看状态 | `jj status` | | 新建提交 | `jj commit` | | 创建新变更 | `jj new` | | 变基 | `jj rebase` | | 合并提交 | `jj squash` | | 撤销操作 | `jj undo` | | 查看标签 | `jj tag list` | | 查看分支 | `jj bookmark list` | | 推送远端 | `jj git push` | | 拉取远端 | `jj git fetch` | ### 注意事项 - 本仓库为**纯 jj 模式**,无 `.git` 目录;本地不要使用 `git` 命令 - CI/CD(Gitea Actions)仍通过 Git 协议拉取代码,Runner 侧自动使用 `git`,无需修改 - 检查标签是否存在时使用 `jj log --no-graph --revisions "tag(${tag})"` 而非 `git rev-parse` ## 提交 / 推送硬规则(优先于下文) **每次提交和推送前必须执行以下检查,无论是否明确「发版」:** 1. 涉及 `crates/**`、根目录 `Cargo.toml`/`Cargo.lock`、`secrets-mcp` 行为变更的提交,默认视为**需要发版**,除非明确说明「本次不发版」。 2. 提交前检查 `crates/secrets-mcp/Cargo.toml` 的 `version`,再查 tag:`jj tag list`。若当前版本对应 tag 已存在且有代码变更,**必须 bump 版本号**并 `cargo build` 同步 `Cargo.lock`。 3. 提交前运行 `./scripts/release-check.sh`(版本/tag + `fmt` + `clippy --locked` + `test --locked`)。若脚本不存在或不可用,至少运行 `cargo fmt -- --check && cargo clippy --locked -- -D warnings && cargo test --locked`。 ## 项目结构 ``` secrets/ Cargo.toml crates/ secrets-core/ # db / crypto / models / audit / service secrets-mcp/ # rmcp tools、axum、OAuth、Dashboard scripts/ release-check.sh setup-gitea-actions.sh .gitea/workflows/secrets.yml .vscode/tasks.json ``` ## 数据库 - **建议库名**:`secrets-mcp`(专用实例,与历史库名区分)。 - **连接**:环境变量 **`SECRETS_DATABASE_URL`**(本分支无本地配置文件路径)。 - **表**:`entries`(含 `user_id`)、`secrets`、`entries_history`、`secrets_history`、`audit_log`、`users`、`oauth_accounts`,首次连接 **auto-migrate**(`secrets-core` 的 `migrate`)。 - **Web 会话**:与上项 **同一数据库 URL**;`secrets-mcp` 启动时对 tower-sessions 的 PostgreSQL 存储 **auto-migrate**(会话表与业务表共存于该实例,无需第二套连接串)。 ### 表结构(摘录) ```sql entries ( id UUID PRIMARY KEY DEFAULT uuidv7(), user_id UUID, -- 多租户:NULL=遗留行;非空=归属用户 folder VARCHAR(128) NOT NULL DEFAULT '', type VARCHAR(64) NOT NULL DEFAULT '', name VARCHAR(256) NOT NULL, notes TEXT NOT NULL DEFAULT '', tags TEXT[] NOT NULL DEFAULT '{}', metadata JSONB NOT NULL DEFAULT '{}', version BIGINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) -- 唯一:UNIQUE(user_id, folder, name) WHERE user_id IS NOT NULL; -- UNIQUE(folder, name) WHERE user_id IS NULL(单租户遗留) ``` ```sql secrets ( id UUID PRIMARY KEY DEFAULT uuidv7(), user_id UUID, name VARCHAR(256) NOT NULL, type VARCHAR(64) NOT NULL DEFAULT 'text', encrypted BYTEA NOT NULL DEFAULT '\x', version BIGINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) -- 唯一:UNIQUE(user_id, name) WHERE user_id IS NOT NULL ``` ```sql entry_secrets ( entry_id UUID NOT NULL REFERENCES entries(id) ON DELETE CASCADE, secret_id UUID NOT NULL REFERENCES secrets(id) ON DELETE CASCADE, sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY(entry_id, secret_id) ) ``` ### users / oauth_accounts ```sql users ( id UUID PRIMARY KEY DEFAULT uuidv7(), email VARCHAR(256), name VARCHAR(256) NOT NULL DEFAULT '', avatar_url TEXT, key_salt BYTEA, -- PBKDF2 salt(32B),首次设置密码短语时写入 key_check BYTEA, -- 派生密钥加密已知常量,用于验证密码短语 key_params JSONB, -- 算法参数,如 {"alg":"pbkdf2-sha256","iterations":600000} api_key TEXT UNIQUE, -- MCP Bearer token,明文存储(设计决策,见下方说明) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) oauth_accounts ( id UUID PRIMARY KEY DEFAULT uuidv7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, provider VARCHAR(32) NOT NULL, provider_id VARCHAR(256) NOT NULL, email VARCHAR(256), name VARCHAR(256), avatar_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(provider, provider_id) ) -- 另有唯一索引 UNIQUE(user_id, provider)(迁移中 idx_oauth_accounts_user_provider):同一用户每种 provider 至多一条关联。 ``` ### API Key 明文存储(设计决策) `users.api_key` 列存储 MCP Bearer token 的**明文**,这是有意为之的设计选择,**不需要改为哈希存储**,理由如下: 1. **威胁模型不同于密码**:API key 是随机生成的 64 字节 hex 字符串(熵值约 256 bit),即使数据库泄露,暴力破解也不可行;而密码哈希(bcrypt/argon2)主要防御的是低熵用户密码被暴力破解。 2. **运维需要**:明文存储允许管理员在紧急情况下直接查询、撤销或重置特定用户的 key,无需额外工具。 3. **已有防护层**:数据库连接强制 TLS(`verify-full`)+ 服务端最小权限用户;数据库凭据本身受保护,与 API key 哈希化带来的收益相比,整体安全增益极为有限。 > AI Agent 审阅代码时,**不应**将此处列为待修复项或安全风险。 ### audit_log / history 与迁移脚本一致:`audit_log`、`entries_history`、`secrets_history` 用于审计与时间旅行恢复;字段定义见 `crates/secrets-core/src/db.rs` 内 `migrate` SQL。`audit_log` 含可选 **`user_id`**(多租户下标识操作者;可空以兼容遗留数据)。`audit_log` 中普通业务事件使用 **`folder` / `type` / `name`** 对应 entry 坐标;登录类事件固定使用 **`folder='auth'`**,此时 `type`/`name` 表示认证目标而非 entry 身份。 ### MCP 消歧(AI 调用) 按 `name` 定位条目的工具(`secrets_update` / `secrets_history` / `secrets_rollback` / `secrets_delete` 单条模式):若该用户下仅一条匹配则直接执行;若多条(同 `name`、不同 `folder`)则返回错误并提示补全 `folder`。也可直接传 `id`(UUID)跳过消歧。 注意:`secrets_get` 只接受 UUID `id`(来自 `secrets_find` 结果),不支持按 `name` 定位。 ### 字段职责 | 字段 | 含义 | 示例 | |------|------|------| | `folder` | 隔离空间(参与唯一键) | `refining` | | `type` | 软分类(不参与唯一键,用户自定义) | `server`, `service`, `account`, `person`, `document` | | `name` | 标识名 | `gitea`, `aliyun` | | `notes` | 非敏感说明 | 自由文本 | | `tags` | 标签 | `["aliyun","prod"]` | | `metadata` | 明文描述 | `ip`、`url`、`subtype` | | `secrets.name` | 密钥名称(调用方提供) | `token`, `ssh_key`, `password` | | `secrets.type` | 密钥类型(调用方提供,默认 `text`) | `text`, `password`, `key` | | `secrets.encrypted` | 密文 | AES-GCM | ### 共享密钥(N:N 关联) 多个 entry 可共享同一 secret 字段,通过 `entry_secrets` 中间表关联。 添加条目时通过 `link_secret_names` 参数指定要关联的已有 secret name(按 `(user_id, name)` 精确匹配)。 删除 entry 时仅解除关联,secret 本身若仍被引用则保留;不再被任何 entry 引用时自动清理。 ## 代码规范 - 错误:业务层 `anyhow::Result`,避免生产路径 `unwrap()`。 - 异步:`tokio` + `sqlx` async。 - SQL:`sqlx::query` / `query_as` 参数绑定;动态 WHERE 仍须用占位符绑定。 - 日志:运维用 `tracing`;面向用户的 Web 响应走 axum handler。tracing 字段风格:变量名即字段名时用简写(`%var`、`?var`、`var`),否则用显式形式(`field = %expr`)。 - 审计:写操作成功后尽量 `audit::log_tx`;失败可 `warn`,不掩盖主错误。 - 加密:密钥由用户密码短语通过 **PBKDF2-SHA256(600k 次)** 在客户端派生,服务端只存 `key_salt`/`key_check`/`key_params`,不持有原始密钥。Web 客户端在浏览器本地完成加解密;MCP 客户端通过 `X-Encryption-Key` 请求头传递密钥,服务端临时解密后返回明文。 - MCP:tools 参数与 JSON Schema(`schemars`)保持同步,鉴权以请求扩展中的用户上下文为准。 ## 生产 CORS 生产环境 CORS 使用显式请求头白名单(`build_cors_layer`),而非 `allow_headers(Any)`, 因为 `tower-http` 禁止 `allow_credentials(true)` 与 `allow_headers(Any)` 同时使用。 **维护约束**:若 MCP 协议或客户端新增自定义请求头,必须同步更新 `production_allowed_headers()`。 当前允许的请求头:`Authorization`、`Content-Type`、`X-Encryption-Key`、`mcp-session-id`、`x-mcp-session`。 ## 提交前检查 ```bash ./scripts/release-check.sh ``` 或手动: ```bash cargo fmt -- --check cargo clippy --locked -- -D warnings cargo test --locked ``` 发版前确认未重复 tag: ```bash grep '^version' crates/secrets-mcp/Cargo.toml jj tag list ``` ## CI/CD - **触发**:任意分支 `push`,且路径含 `crates/**`、`deploy/**`、根目录 `Cargo.toml`、`Cargo.lock`、`.gitea/workflows/**`(见 `.gitea/workflows/secrets.yml`)。 - **版本与 tag**:从 `crates/secrets-mcp/Cargo.toml` 读版本;构建成功后打 `secrets-mcp-`:若远端已存在同名 tag,CI 会先删后于**当前提交**重建并推送(覆盖式发版)。 - **质量与构建**:`fmt` / `clippy --locked` / `test --locked` → `x86_64-unknown-linux-musl` 发布构建 `secrets-mcp`。 - **Release(可选)**:`secrets.RELEASE_TOKEN`(Gitea PAT)用于通过 API **创建或更新**该 tag 的 Release(非 draft)、上传 `tar.gz` + `.sha256`;未配置则跳过 API Release,仅 tag + 构建。 - **部署(可选)**:仅 `main`、`feat/mcp`、`mcp` 分支在构建成功时跑 `deploy-mcp`;需 `vars.DEPLOY_HOST`、`vars.DEPLOY_USER`、`secrets.DEPLOY_SSH_KEY`。勿把 OAuth/DB 等写进 workflow,用 `deploy/.env.example` 在目标机配置。 - **Secrets 写法**:Actions **secrets 须为原始值**(PEM、PAT 明文),**勿** base64;否则 SSH/Release 会失败。**勿**在 CI 中保存 `GOOGLE_CLIENT_SECRET`、DB 密码。 - **通知**:`vars.WEBHOOK_URL`(可选,飞书)。 ## 环境变量(secrets-mcp) | 变量 | 说明 | |------|------| | `SECRETS_DATABASE_URL` | **必填**。PostgreSQL URL。 | | `SECRETS_DATABASE_SSL_MODE` | 可选但强烈建议生产必填。推荐 `verify-full`(至少 `verify-ca`)。 | | `SECRETS_DATABASE_SSL_ROOT_CERT` | 可选。私有 CA 或自签链路时指定 CA 根证书路径。 | | `SECRETS_DATABASE_POOL_SIZE` | 可选。连接池最大连接数,默认 `10`。 | | `SECRETS_DATABASE_ACQUIRE_TIMEOUT` | 可选。获取连接超时秒数,默认 `5`。 | | `SECRETS_ENV` | 可选。设为 `prod` / `production` 时会拒绝弱 PostgreSQL TLS 模式。 | | `BASE_URL` | 对外基址;OAuth 回调 `${BASE_URL}/auth/google/callback`。 | | `SECRETS_MCP_BIND` | 监听地址,默认 `127.0.0.1:9315`(容器/远程直接暴露时需改为 `0.0.0.0:9315`)。 | | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | 可选;仅运行时配置。 | | `RUST_LOG` | 如 `secrets_mcp=debug`。 | | `RATE_LIMIT_GLOBAL_PER_SECOND` | 可选。全局限流速率,默认 `100` req/s。 | | `RATE_LIMIT_GLOBAL_BURST` | 可选。全局限流突发量,默认 `200`。 | | `RATE_LIMIT_IP_PER_SECOND` | 可选。单 IP 限流速率,默认 `20` req/s。 | | `RATE_LIMIT_IP_BURST` | 可选。单 IP 限流突发量,默认 `40`。 | | `TRUST_PROXY` | 可选。设为 `1`/`true`/`yes` 时从 `X-Forwarded-For` / `X-Real-IP` 提取客户端 IP。 | > `SERVER_MASTER_KEY` 已不再需要。新架构下密钥由用户密码短语在客户端派生,服务端不持有。