farchanjo/merkle
GitHub: farchanjo/merkle
Merkle 是一个本地优先的 MCP 密钥保险库,通过不透明句柄和人工审批机制让 LLM 安全使用凭证而永不接触明文。
Stars: 2 | Forks: 0
# 🔐 Merkle
一个**本地优先的 MCP 密钥保险库**,它交给你的 LLM 不透明的*句柄*——绝非明文。每一次 reveal 都需要人类批准,每一次访问都会被写入防篡改的哈希链。100% 在你的机器上运行。
[](https://github.com/farchanjo/merkle)
[](LICENSE)
[](rust-toolchain.toml)
[](Cargo.toml)
[](docs/arch/integrations/claude-code-wiring.md)
[](#-development)
[-lightgrey)](#project-status)
## 什么是 Merkle?
LLM 编程代理非常出色——直到它们需要密钥。当生产密码或 SSH key 进入模型的 context window 时,它就已经被泄露给提供商、缓存在记录中,并且极有可能被记录下来了。你无法“撤回”已发送的密钥。
**Merkle 改变了这个契约。** 你不再给模型一个密钥,而是给它一个不透明的**句柄**,比如 `vault://acme/password/db-prod`。模型可以*使用*该句柄——运行 SSH 命令、发起 HTTPS 调用、为子进程实体化临时文件——但明文仅会在本地 daemon 内部解引用,并且**永远不会跨越 MCP 传输层**。当确实必须显示明文时,必须由**人类**在带外批准,并且该行为会被永久记录。
三大保证支撑着这一切:
1. 🪪 **LLM 持有句柄,而非密钥。** 代理工具操作凭证;仅返回过滤后的结果。
2. 🙋 **人类批准每一次显示。** 授权通过模型无法触及的通道传达。
3. 🔗 **所有操作都在防篡改哈希链中审计。** 截断或重写它都会导致验证失败。
而且它是 **100% 本地**的——一个长期运行的 daemon 拥有你的密钥、SQLite 数据库和审计链。不会有任何数据发回外部。
## 为什么选择 Merkle?
| 没有保险库 | 🔐 使用 Merkle |
|---|---|
| 粘贴到 prompt 中的密钥会泄露给提供商、记录和日志 | LLM 只能看到不透明的**句柄**;明文保留在本地 daemon 中 |
| 模型可以“决定”读取密钥 | 每次 reveal 都需要模型无法伪造的**操作员确认** |
| 泄露一个 key = 静默被攻破 | **高敏感度**的密钥需要**带外 (OOB)** 的人工确认 |
| 没有记录谁在何时访问了什么 | 带有固定头端的 **BLAKE3 哈希链**使得篡改可被检测 |
| 云端密钥管理器、网络往返、出口风险 | **本地优先**:SQLite + OS keychain,Unix socket IPC,无网络依赖 |
| 沉重的 runtime 和 sidecars | 三个小巧的**静态 Rust 二进制文件**——agent、CLI、MCP server |
## 工作原理
Merkle 采用**六边形架构**构建(DDD + ports/adapters,参见 [ADR-0002](docs/arch/adr/0002-adopt-agent-plus-mcp-adapter-topology.md) / [ADR-0024](docs/arch/adr/0024-mcp-adapter-consumes-companion-socket-client.md))。领域核心从不导入基础设施。
这与您已经在 `ssh-agent` 和 `gpg-agent` 中信任的模型相同:**一个长期运行的 daemon 拥有所有危险状态**,其他一切都是通过 Unix domain socket 与它通信的瘦客户端。
### 进程拓扑结构
```
flowchart LR
CC["🖥️ Claude Code
(any window)"] OP["👤 Operator"] MCP["merkle-mcp
(stdio MCP server)"] CLI["merkle
(operator CLI)"] AG["merkle-agent
(daemon / composition root)"] APP["merkle-application
AppContext"] SQL[("SQLite
vault.db")] KC[("OS keychain /
keystore.age")] AUD["Audit hash chain
+ PinnedHead"] CC -- "JSON-RPC 2.0 / stdio" --> MCP OP --> CLI MCP -- "HTTP/1.1 over Unix socket" --> AG CLI -- "HTTP/1.1 over Unix socket" --> AG AG --> APP APP --> SQL APP --> KC APP --> AUD ``` ### 六个限界上下文 ``` flowchart LR IdentityAndSealing["🔑 Identity & Sealing"] SecretStorage["🗄️ Secret Storage"] AccessMediation["🚪 Access Mediation"] AuditCompliance["📋 Audit & Compliance"] BackupRecovery["💾 Backup & Recovery"] PolicyPermissions["📜 Policy & Permissions"] IdentityAndSealing -->|"unwraps DEKs for"| SecretStorage SecretStorage -->|"resolved by"| AccessMediation AccessMediation -->|"chains entries via"| AuditCompliance SecretStorage -->|"snapshotted by"| BackupRecovery PolicyPermissions -->|"governs"| AccessMediation PolicyPermissions -->|"governs"| SecretStorage ``` | 限界上下文 | 一句话职责 | |---|---| | **身份与封存** | 掌管密钥层级 (MasterKey → VaultRootKey → NamespaceDEKs) 以及封存/解封状态机 | | **密钥存储** | 为 `Secret` 聚合根建模,包含版本控制、分类、敏感度以及每个 blob 的 AEAD 加密 | | **访问中介** | 评估 reveal/代理请求;签发 `UseToken`;管理 OOB 确认和 Companion Devices | | **审计与合规** | 通过保存在 SQLite 中由 HMAC 加密的 `PinnedHead` 维护防篡改的 BLAKE3 哈希链 | | **备份与恢复** | Anacron 风格的调度器;`age` 加密的双接收者导出;灾难恢复还原计划 | | **策略与权限** | Namespace 策略、速率限制、reveal 策略、跨 namespace 访问控制、安全配置 | ### Ports & adapters 概览 - **一个入站(驱动)port** —— 即 **Companion Socket**:基于 Unix domain socket 的 HTTP/1.1(约 33 个路径 / 35 个操作),对端凭证认证(仅限相同 UID),每请求 30 秒超时,最大 8 MiB body。 - **五个出站(被驱动)ports** —— `StoragePort`、`KeychainPort`、`CryptoPort`、`OobNotifierPort`、`ExternalServicesPort`,分别由 SQLite、keychain、加密、OOB 和外部服务 adapters 实现。 请在 [Workspace layout](#workspace-layout) 中查看完整的 crate 映射。 ## Reveal 模型 这是 Merkle 安全设计的核心:**LLM 永远无法授权自己的 reveal。** reveal 受到两个独立标志的门控限制——而模型对这两者都无法控制(参见 [ADR-0011](docs/arch/adr/0011-slash-only-reveal-with-oob-for-high-sensitivity.md))。 | 标志 | 设置者 | 通道 | 适用条件 | |:---|:---|:---|:---| | `slash_command` | Claude Code **客户端** | MCP 会话上下文 —— *非*工具参数 | **所有**敏感度级别 | | `oob_ack` | **OOB 通知器** | 独立的 OS 通道(桌面通知 / TTY 提示 / `localhost` 确认) | `sensitivity=high`(**与** `slash_command` 同时满足) | 在此基础之上,`vault_reveal` 和 `vault_delete` 还会查询第三个独立信号:在 MCP `_meta` 字段中,由 key `dev.farchanjo.merkle/operator_confirmation` (MERK-001) 携带的**操作员确认**。LLM 只能控制工具 `arguments` 对象;`_meta` 是由客户端传输层写入的。`VaultRevealInput` 和 `VaultDeleteInput` 故意**没有** `operator_confirmation` 字段——因此试图通过参数设置它的模型将毫无作用。 ### 高敏感度 reveal 的端到端流程 ``` sequenceDiagram actor Operator participant CC as Claude Code Client participant LLM as LLM Context participant MCP as merkle-mcp (stdio) participant Agent as merkle-agent (daemon) participant OOB as OOB Notifier Operator->>CC: types /merkle-reveal {handle} "reason" CC->>LLM: injects slash_command + operator_confirmation (_meta) LLM->>MCP: vault_reveal { handle, purpose } Note over LLM,MCP: no operator_confirmation in arguments MCP->>Agent: POST /v1/reveal (slash_command=true) Agent->>Agent: verify slash_command + sensitivity=high Agent->>OOB: issue OobChallenge (id, nonce, expires_at) OOB->>Operator: desktop notification / TTY / localhost page Operator->>OOB: Approve -> Ed25519 sign(id, nonce) OOB->>Agent: OobResolution { oob_ack=true, signature } Agent->>Agent: verify Ed25519 + nonce binding + rate limit Agent->>Agent: append audit entry (pre-commit) Agent->>MCP: plaintext MCP->>LLM: tool result ``` 对于低/中等敏感度,OOB 步骤将被跳过;仅需要 `slash_command`(和操作员确认)。如果需要 OOB 但尚未确认,agent 会返回 `{ "oob_pending": true, "oob_channel": ..., "expires_at": ..., "request_nonce": ... }`,调用方在确认后重新发起请求。 ## 封存与解封生命周期 在冷启动时,保险库处于**封存** 状态——Vault Root Key (VRK) 不在内存中,所有的读写操作都会被拒绝。解封会加载并解密 VRK;封存则会将其清零。 ``` stateDiagram-v2 [*] --> Sealed : vault init / cold start Sealed --> Unsealing : merkle unseal (begin write-lock) Unsealing --> Sealed : any failure (keychain / AEAD / HMAC) — rollback Unsealing --> Unsealed : HMAC key published + state flip + audit op=Unseal Unsealed --> ShuttingDown : merkle seal / SIGTERM ShuttingDown --> Sealed : drain complete (30s hard timeout) note right of Unsealing nonce(24) || ciphertext AEAD-decrypted AAD = b"vault-root-key" HMAC key published BEFORE state flip Direct Sealed → Unsealed is REJECTED end note ``` **解封操作顺序:** 获取写锁 → 从 keychain 读取 `vrk-master-v1` blob → AEAD 解密它 (AAD `b"vault-root-key"`) → 派生审计 HMAC key `BLAKE3_keyed(vrk_bytes, b"merkle vault hmac key v1")` → **在**状态切换为 `Unsealed` 之前**发布 HMAC key** → 追加一条 `op=Unseal` 记录。在读取与发布之间发生的任何失败都会回滚到 `Sealed` 状态。 ## 审计哈希链 对密钥的每一项操作都会被记录在只能追加、防篡改的链中——将 Ralph Merkle 的哈希链应用于你的审计日志。 ``` flowchart LR G["Genesis
prev_hash = blake3:0000…0000
hmac = keyed(hash ‖ id)"] E1["Entry 1
hash = BLAKE3(canonical ‖ prev)
hmac = keyed(hash ‖ id)"] E2["Entry 2
hash = BLAKE3(canonical ‖ E1.hash)
hmac = keyed(hash ‖ id)"] PH[("PinnedHead
head_hash ‖ head_seq ‖ entry_count
MAC under same key")] G --> E1 --> E2 --> PH ``` - **无密钥哈希:** `current_hash = BLAKE3(canonical_fields || prev_hash)` —— 编辑任何条目都会导致后续所有哈希值产生分歧。 - **带密钥 HMAC:** `hmac = BLAKE3_keyed(hmac_key, current_hash || id_uuid)` —— 密钥在解封时从 VRK 字节派生,且绝不单独持久化存储。 - **PinnedHead**(一行单独的 SQLite 记录)将链尖端绑定在相同的密钥下,挫败*截断后重建*攻击。 - `ChainVerifier::verify_full` 会报告精确的拒绝结果:`GenesisAnchorMissing`、`BrokenAtEntry`、`HmacMismatch`、`MissingHmac`、`HeadMacMismatch`、`TruncationDetected`、`HeadHashMismatch` 等。 ## 加密与安全 ### 基础组件 | 组件 | Crate | 用途 | |:---|:---|:---| | XChaCha20-Poly1305 | `chacha20poly1305` | 每个 blob 的 AEAD 加密 (24 字节 nonce,16 字节 tag) | | BLAKE3 hash + keyed | `blake3` | 审计哈希链完整性,HMAC 替代品,KDF (XOF) | | Argon2id (RFC 9106) | `argon2` | 密码 KDF —— 最低限制 `m_cost≥65536`,`t_cost≥3`,`p_cost≥1` | | age / X25519 | `age` 0.11 | 双接收者备份加密 + 文件密钥库 (scrypt `log_n` 固定为 18) | | Ed25519 | `ed25519-dalek` | OOB 解析签名,操作员证明 JWTs | | X25519 ECIES | `x25519-dalek` | OOB 挑战载荷加密 ([ADR-0019](docs/arch/adr/0019-ecies-encryption-for-oob-challenge-payload.md)) | | OsRng | `rand` 0.10 | 所有密钥/nonce 生成 —— 熵门控 `assert_entropy_gate()` | 不使用 OpenSSL。所有基础组件均为纯 Rust 实现(`deny.toml` 中禁止使用 `openssl`/`openssl-sys`/`git2`)。 ### 关键保证 - **Zeroize:** `MasterKey`、`VaultRootKey`、`NamespaceDek` 和 `PrivateBlob` 在 drop 时会被清零;`Debug` 被重写为输出 `[REDACTED]`;`MasterKey::clone()` 故意**不**复制密钥字节。 - **SSRF / DNS-rebind 防御:** `DestinationPolicy::strict()` 拒绝非 HTTPS、loopback、link-local(包括 `169.254.x.x` 云元数据 IP)、private、CGNAT、multicast 和 IPv6 ULA/link-local —— *在*附加凭证*之前*。`ValidatingDnsResolver` 在连接时再次应用相同的检查,以消除 TOCTOU 漏洞,并 fail closed。 - **对端凭证 socket 认证 (GAP-007):** 父目录强制为 `0700`,绑定期间 umask 为 `0177`,显式对 socket 执行 `chmod 0600`。macOS 使用 `LOCAL_PEERCRED`,Linux 使用 `SO_PEERCRED` + `/proc//exe`;仅限相同 UID,并在**每个**请求上进行验证。
- **安全配置** (`[security] security_profile`):
| 配置 | OOB 要求 | 空闲超时 | mlock |
|:---|:---|:---|:---|
| `relaxed` | 无 | 可配置 | 可选 |
| `balanced` *(默认)* | `sensitivity=high` | 30 分钟 (1800 秒) | 推荐 |
| `paranoid` | 所有敏感度 | 5 分钟 | 必须 |
- **备份不变量 ([ADR-0006](docs/arch/adr/0006-age-encryption-for-backups-and-recovery.md)):** 恰好两个不同的 age 接收者 (MasterPubkey + RecoveryPublicKey),`secret_count > 0`,**encrypt-then-MAC** (对 age 密文使用的 BLAKE3 keyed MAC)。文件名 `merkle-bk-.merkle.age`。
- **发布不变量:** `panic = "abort"` —— 在破坏/违反不变量的状态下禁止 unwind。永远不要改变它。
完整分析位于 [STRIDE 威胁模型](docs/arch/threat-model/stride-analysis.md) 和 [SECURITY.md](SECURITY.md)。
## 🚀 快速开始
### 前置条件
- Rust **1.89**,MSRV **1.89**(通过 `rust-toolchain.toml` 固定)
- Linux 建依赖:`libsqlite3-dev pkg-config libdbus-1-dev`(`keyring` 的 Secret Service 后端需要 `libdbus-1-dev`)
- macOS / Windows:只需 Rust 工具链(SQLite 已内置)
### 构建
```
git clone https://github.com/farchanjo/merkle.git
cd merkle
cargo build --release # produces target/release/{merkle, merkle-agent, merkle-mcp}
```
### 启动 agent
daemon 必须在你初始化**之前**运行——`merkle init` 会通过 Companion Socket 发送初始化仪式,因此 agent 必须启动才能接收并执行它。
```
cargo run -p merkle-agent # dev mode (or: make agent)
```
在 macOS 上,你通常会以用户级 LaunchAgent 的形式运行 agent——请参阅 [LaunchAgent 设置](#-development) 以及 `deploy/launchd/` 中的资源。
### 初始化保险库
```
merkle init # generates the vault and prints the Recovery Key ONCE
```
### 解封并确认
```
merkle unseal # load the Vault Root Key into protected memory
merkle status # confirm sealed=false
```
### 存入你的第一个密钥
```
merkle bind acme # create + bind the "acme" namespace
# 中等敏感度 API token
echo -n "ghp_supersecrettoken" | merkle put vault://acme/token/github-pat \
--sensitivity medium --tag project:my-app --tag role:ci \
--description "GitHub PAT for CI"
# 高敏感度密码(必须带有 env:* tag,禁止 --expose)
echo -n "hunter2" | merkle put vault://acme/password/db-prod \
--sensitivity high --tag env:prod --tag role:backend
```
### 连接 Claude Code
将以下内容添加到 `~/.claude.json`:
```
{
"mcpServers": {
"merkle": {
"command": "/usr/local/bin/merkle-mcp",
"args": []
}
}
}
```
然后重启 Claude Code(或运行 `/mcp restart merkle`),并通过询问 Claude 来验证:
你应该会看到 `"sealed": false`。你已连接成功。
| 可选环境变量 | 默认值 | 用途 |
|---|---|---|
| `MERKLE_SOCKET` | 自动发现 | 覆盖 Companion Socket 路径 |
| `MERKLE_LOG` | `info` | stderr 日志级别 |
## 🔧 CLI 参考
`merkle` 二进制文件是操作员 CLI。它仅通过 Companion Socket 与正在运行的 `merkle-agent` 通信,且绝不直接触碰密钥或存储。
**全局标志**(适用于所有子命令):
| 标志 | 环境变量 | 默认值 | 用途 |
|---|---|---|---|
| `--socket ` | `MERKLE_SOCKET` | 平台默认值 | 覆盖 Companion Socket 路径 |
| `--output ` / `-o` | — | `human` | `human`、`json` 或 `plain` |
**生命周期**
| 命令 | 功能说明 |
|---|---|
| `merkle init [--non-interactive]` | 首次运行仪式——生成保险库,**仅打印一次**恢复密钥。`--non-interactive` 抑制 TTY 提示 (CI)。 |
| `merkle unseal [--passphrase]` | 将 VRK 加载到内存中 (Sealed → Unsealed)。`--passphrase` 从 TTY 读取而不是从 OS keychain。 |
| `merkle seal` | 将 VRK 清零 (Unsealed → Sealed)。 |
| `merkle status` | Agent 健康状况、封存状态、版本号。 |
**Namespaces 与 secrets**
| 命令 | 功能说明 |
|---|---|
| `merkle bind ` | 将 namespace 绑定到会话(如果不存在则创建)。在执行 `put`/`list`/`get` 之前必须执行此操作。 |
| `merkle put [flags]` | 创建或覆盖 secret(从 stdin 读取明文)。 |
| `merkle list [] [flags]` | 列出密钥(支持 `--filter`、`--category`、`--sensitivity`、`--limit`,默认 50)。 |
| `merkle get [--reason ]` | 获取一个单次使用的访问 token(无明文),适用于低/中等敏感度密钥。 |
| `merkle describe ` | 显示只读的公开元数据(分类、敏感度、标签、版本)。 |
| `merkle reveal --reason ` | 显示明文。对 `high` 密钥触发 OOB;如果 OOB 待处理,则打印重新运行提示。 |
| `merkle rotate [--purpose ] [--base64]` | 轮换活动值(从 stdin 读取新 payload;版本号严格递增)。 |
| `merkle delete --confirm` | 永久删除密钥及其所有版本。必须提供 `--confirm`。 |
| `merkle search [--limit N]` | FTS5 全文搜索(默认限制 20)。 |
**`put` 标志**
| 标志 | 默认值 | 备注 |
|---|---|---|
| `--sensitivity ` | `medium` | `high` 要求至少有一个 `--tag env:*` 且禁止使用 `--expose` |
| `--tag ` | — | 可重复使用。允许的 key:`env`、`project`、`role`、`provider`、`team` |
| `--category ` | `note` | 创建后**不可变** |
| `--description ` | — | 公开元数据备注 |
| `--force` | false | 覆盖同名的现有密钥 |
| `--base64` | false | 将 stdin 视为 Base64 二进制(SSH keys、TLS certs、JWK blobs) |
句柄格式:`vault:////` 或简写形式 `//`。
**审计、备份、还原、设备、诊断**
| 命令 | 功能说明 |
|---|---|
| `merkle audit [--op ] [--since ] [--limit N]` | 查询只追加的审计日志(默认限制 50)。 |
| `merkle backup now [--note ]` | 触发按需备份。 |
| `merkle backup list [] [--limit N]` | 列出备份快照(默认限制 20)。 |
| `merkle restore plan [--mode ]` | 预览还原计划(试运行)。 |
| `merkle restore execute ` | 执行先前创建的还原计划。 |
| `merkle device pair --name --class ` | 配对一个 Companion Device;打印配对码。 |
| `merkle device list` | 列出已注册的 Companion Devices。 |
| `merkle device revoke ` | 吊销一个 Companion Device。 |
| `merkle verify-recovery-key [--identity-file ]` | 验证存储的恢复密钥是否对应于 `recovery_pubkey`。默认从 TTY 读取(关闭回显)。 |
| `merkle doctor [--durability] [--chain] [--all]` | 自我诊断。`--chain` 验证审计哈希链;`--all` 运行所有检查。 |
## 🧰 MCP 工具与 prompts
`merkle-mcp` 是一个轻量级的 stdio MCP server (rmcp 1.8)。每个 Claude Code 窗口对应一个进程;它将所有调用代理给 daemon,且从不导入领域或存储代码。它暴露了 **30 个工具**和 **4 个 prompts**。
| 分组 | 工具 |
|---|---|
| **身份** (3) | `vault_unseal` · `vault_seal` · `vault_bind` |
| **密钥** (8) | `vault_put` · `vault_get` · `vault_list` · `vault_describe` · `vault_rotate` · `vault_delete` · `vault_search` · `vault_history` |
| **Reveal** (1) | `vault_reveal` ⚠️ 需要操作员确认 |
| **使用 token** (4) | `vault_use` · `vault_write_tempfile` · `vault_write_fifo` · `vault_revoke_tempfile` |
| **代理** (10) | `vault_ssh_exec` · `vault_ssh_copy` · `vault_ssh_port_forward` · `vault_ssh_shell`\* · `vault_http_request` · `vault_http_download` · `vault_http_upload` · `vault_spawn` · `vault_crypto_sign` · `vault_crypto_decrypt` |
| **审计** (1) | `vault_audit_query` |
| **备份** (2) | `vault_backup` · `vault_restore` |
| **诊断** (1) | `vault_doctor` |
**4 个 prompts** —— 在 Claude Code 中以 `/mcp__merkle__` 的形式呈现 ([ADR-0028](docs/arch/adr/0028-mcp-prompts-for-slash-commands.md)):
| Prompt | 映射到 | 需要操作员确认? |
|---|---|---|
| `merkle-doctor` | `vault_doctor` | 否 |
| `merkle-show` | `vault_describe` | 否 |
| `merkle-reveal` | `vault_reveal` | **是** |
| `merkle-rollback` | `vault_history` + `vault_rotate` | **是** |
每个会话都应从 `vault_bind { label }` 开始(每个会话最多执行一次,根据 [ADR-0026](docs/arch/adr/0026-idempotent-bind-and-session-state-atomicity.md) 进行两阶段提交)。`cwd_hash` 在内部通过工作目录的 BLAKE3 派生,且绝不会跨越传输层。完整细节请参阅 [Claude Code 连接指南](docs/arch/integrations/claude-code-wiring.md)。
## 📚 文档
### 索引
- [什么是 Merkle?](#what-is-merkle)
- [工作原理](#how-it-works)
- [快速开始](#-quick-start)
- [文档](#-documentation)
- [开发](#-development)
- [加密与安全](#crypto--security)
架构的**事实来源**是 [`docs/arch/`](docs/arch/)。
[speckit 控制平面](doc/arch/)位于 `doc/arch/` 下(通过符号链接指向
`docs/arch/` 中的 ADR、schemas、features 和相关切片)。通过
`~/bin/speckit status` / `next` / `validate` 驱动 SDD。
| 领域 | 路径 | 内容 |
|---|---|---|
| Speckit 平面 | [doc/arch/](doc/arch/) | 基础宪章、治理、SDD、符号链接 |
| 架构概览 | [docs/arch/README.md](docs/arch/README.md) | DDD + 六边形堆栈,目录布局 |
| 术语表 | [docs/arch/glossary.md](docs/arch/glossary.md) | 按限界上下文划分的标准词汇 |
| ADRs | [docs/arch/adr/](docs/arch/adr) | MADR 决策记录 `0001`–`0032` |
| 领域叙事 | [docs/arch/domain/](docs/arch/domain) | 限界上下文模型和不变量 |
| CUE schemas (50) | [docs/arch/schemas/](docs/arch/schemas) | DTO 和分类的类型契约 |
| Rego policies (18) | [docs/arch/policies/](docs/arch/policies) | Conftest 策略门控 |
| Gherkin features (15) | [docs/arch/specs/features/](docs/arch/specs/features) | 验收场景 (Cucumber) |
| 威胁模型 | [docs/arch/threat-model/](docs/arch/threat-model) | STRIDE,信任边界,攻击面 |
| SLOs (43) | [docs/arch/slo/](docs/arch/slo) | OpenSLO 服务级别目标 |
| 形式化模型 (2) | [docs/arch/formal/](docs/arch/formal) | TLA+ 规范 (在完整测试通道中进行 TLC 检查) |
| 集成 | [docs/arch/integrations/](docs/arch/integrations) | Companion Socket OpenAPI,Claude Code 连接,入门指引 |
| C4 模型 | [docs/arch/architecture/workspace.dsl](docs/arch/architecture/workspace.dsl) | Structurizr 工作区 |
## 🧪 开发
### 初始 5 分钟
```
cargo build --workspace # debug build (fast)
cargo test --workspace --no-fail-fast # ~753 pass / 0 fail / ~18 ignored
cargo clippy --workspace --all-targets -- -D warnings # lint (mirrors CI)
~/bin/spec validate # medium lane — must stay 9/9 green
make doctor # check + clippy + test (no spec lane; `make doctor-full` adds it)
```
### `make` 目标
运行 `make help` 获取完整的自带说明列表(包含 build、deploy/codesign、launchd 目标)。
| 目标 | 命令 | 备注 |
|---|---|---|
| `make build` / `make build-release` | `cargo build --workspace [--release]` | Debug / 优化 |
| `make check` | `cargo check --workspace --all-targets` | 快速类型检查 |
| `make test` / `make test-fast` | `cargo test --workspace [--lib --bins]` | 完整 / 跳过文档测试 |
| `make lint` | `cargo clippy --workspace --all-targets -- -D warnings` | CI 门控 |
| `make fmt` / `make fmt-check` | `cargo fmt --all [--check]` | 格式化 / 试运行 |
| `make deny` / `make audit` | `cargo deny check` / `cargo audit` | 许可证+禁用项 / 安全公告 |
| `make cov` | `cargo llvm-covworkspace --html` | HTML 报告 |
| `make spec-fast` / `make spec-medium` / `make spec` | `~/bin/spec validate [--lane …]` | 4 / 9 / 14 个验证器 |
| `make doctor` | check + clippy + test | 一次性健康门控(无 spec lane —— 请单独运行 `~/bin/spec validate`) |
| `make doctor-full` | doctor + `spec` | doctor 加上完整的 spec lane 一次性运行 |
| `make agent` | `cargo run -p merkle-agent` | 启动 daemon (开发) |
| `make cli ARGS=…` | `cargo run -p merkle-cli -- ` | 运行 CLI |
| `make deploy` | build-release → 签名 → 安装 → 启动 | 仅限 macOS,一次性 release 部署 |
### Spec 通道
spec 门控根据 [ADR-0018](docs/arch/adr/0018-full-coverage-validation-as-architectural-contract.md) 强制执行“spec 即真理”。
| 通道 | 命令 | 时间 | 验证器 | 用途 |
|---|---|---|---|---|
| **fast** | `make spec-fast` | ~1.5 秒 | 4: `lint_cue`, `lint_ddd_role`, `lint_openapi`, `lint_features` | 开发期间的快速健全性检查 |
| **medium** *(默认)* | `make spec-medium` | ~10 秒 | 9: fast + `lint_structurizr`, `lint_md`, `lint_mermaid`, `lint_madr`, `lint_yaml` | **必须保持 9/9 绿灯** |
| **full** | `make spec` | ~60 秒 | 14: medium + `lint_conftest`, `lint_vale`, `lint_slo`, `lint_asyncapi`, `run_tlc` | **CI 合并门控** |
每一个行为变更都必须在与代码**同一个 commit** 中更新 `docs/arch/`。Spec 制品是**锁定**的——修改代码或 spec 以符合要求,切勿修改验证器配置。
### 测试分类
| 层级 | 运行方式 | 备注 |
|---|---|---|
| **单元测试** | `cargo test --workspace --lib` | `#[cfg(test)]` 内联 |
| **集成测试** | `cargo test --workspace` | 每个 crate 的 `tests/`,真实或 mock adapters |
| **BDD** | `cargo test -p merkle-bdd` | Cucumber;即使存在 pending 场景也会退出码为 0 |
| **E2E** | `cargo build --bins && cargo test -p merkle-e2e -- --ignored` | 针对临时目录生成 agent;全部标记为 `#[ignore]` |
| **冒烟测试** | `cargo test -p merkle-cli -- --include-ignored` (等) | 全部标记为 `#[ignore]`;需要运行已解封的 daemon 或环境设置 |
### Workspace 布局
**22 个成员 —— 19 个库 crate + 3 个二进制文件。** Edition 2024,resolver 3,MSRV 1.89。
### Lint 基线
Lint 配置已**锁定**——切勿编辑 `[workspace.lints]`、`clippy.toml` 或 `rust-toolchain.toml`。请修改代码以符合要求。
| 规则 | 级别 | 范围 |
|---|---|---|
| `clippy::all` / `clippy::pedantic` | `deny` (优先级 -1) | workspace |
| `unsafe_code` | `forbid` | workspace (除了 `merkle-adapter-companion-socket`:`deny`) |
| `missing_docs` | `warn` | workspace (除了 `merkle-adapter-companion-socket`:`allow`) |
| `unused_must_use` | `deny` | workspace |
Commit 需遵循 Angular `(): ` 格式;切勿一次性提交所有文件;必须使用 DCO 签名 (`git commit -s`)。
## 🛡️ 安全
发现漏洞?请阅读 **[SECURITY.md](SECURITY.md)** 了解披露流程。完整的威胁分析 (STRIDE,信任边界,攻击面) 位于 [docs/arch/threat-model/](docs/arch/threat-model)。
## 🤝 贡献
欢迎贡献——请参阅 **[CONTRIBUTING.md](CONTRIBUTING.md)** 和 **[行为准则](CODE_OF_CONDUCT.md)**。每一个行为变更都必须在同一个 commit 中更新 `docs/arch/` 中的 spec,并且所有本地门控(`build`、`clippy -D warnings`、`cargo deny`、`spec validate` medium 9/9)必须通过。
## 📄 许可证
基于 **Apache License 2.0** 授权——详见 [LICENSE](LICENSE)。
## 项目状态
Merkle 已经**实现并通过测试**,处于 **1.0 之前阶段**。完整的 22 个 crate workspace 已构建完成;所有本地门控均已通过——`cargo build`、`cargo clippy -D warnings`、`cargo deny` 以及 medium spec lane(9/9 个验证器)——目前有 **~753 个测试通过 / 0 个失败**(外加约 18 个 `#[ignore]` 的 E2E 和冒烟测试)。当前版本为 **0.1.0 (未发布)**。API 和磁盘格式在 1.0 版本之前仍可能发生变化。
(any window)"] OP["👤 Operator"] MCP["merkle-mcp
(stdio MCP server)"] CLI["merkle
(operator CLI)"] AG["merkle-agent
(daemon / composition root)"] APP["merkle-application
AppContext"] SQL[("SQLite
vault.db")] KC[("OS keychain /
keystore.age")] AUD["Audit hash chain
+ PinnedHead"] CC -- "JSON-RPC 2.0 / stdio" --> MCP OP --> CLI MCP -- "HTTP/1.1 over Unix socket" --> AG CLI -- "HTTP/1.1 over Unix socket" --> AG AG --> APP APP --> SQL APP --> KC APP --> AUD ``` ### 六个限界上下文 ``` flowchart LR IdentityAndSealing["🔑 Identity & Sealing"] SecretStorage["🗄️ Secret Storage"] AccessMediation["🚪 Access Mediation"] AuditCompliance["📋 Audit & Compliance"] BackupRecovery["💾 Backup & Recovery"] PolicyPermissions["📜 Policy & Permissions"] IdentityAndSealing -->|"unwraps DEKs for"| SecretStorage SecretStorage -->|"resolved by"| AccessMediation AccessMediation -->|"chains entries via"| AuditCompliance SecretStorage -->|"snapshotted by"| BackupRecovery PolicyPermissions -->|"governs"| AccessMediation PolicyPermissions -->|"governs"| SecretStorage ``` | 限界上下文 | 一句话职责 | |---|---| | **身份与封存** | 掌管密钥层级 (MasterKey → VaultRootKey → NamespaceDEKs) 以及封存/解封状态机 | | **密钥存储** | 为 `Secret` 聚合根建模,包含版本控制、分类、敏感度以及每个 blob 的 AEAD 加密 | | **访问中介** | 评估 reveal/代理请求;签发 `UseToken`;管理 OOB 确认和 Companion Devices | | **审计与合规** | 通过保存在 SQLite 中由 HMAC 加密的 `PinnedHead` 维护防篡改的 BLAKE3 哈希链 | | **备份与恢复** | Anacron 风格的调度器;`age` 加密的双接收者导出;灾难恢复还原计划 | | **策略与权限** | Namespace 策略、速率限制、reveal 策略、跨 namespace 访问控制、安全配置 | ### Ports & adapters 概览 - **一个入站(驱动)port** —— 即 **Companion Socket**:基于 Unix domain socket 的 HTTP/1.1(约 33 个路径 / 35 个操作),对端凭证认证(仅限相同 UID),每请求 30 秒超时,最大 8 MiB body。 - **五个出站(被驱动)ports** —— `StoragePort`、`KeychainPort`、`CryptoPort`、`OobNotifierPort`、`ExternalServicesPort`,分别由 SQLite、keychain、加密、OOB 和外部服务 adapters 实现。 请在 [Workspace layout](#workspace-layout) 中查看完整的 crate 映射。 ## Reveal 模型 这是 Merkle 安全设计的核心:**LLM 永远无法授权自己的 reveal。** reveal 受到两个独立标志的门控限制——而模型对这两者都无法控制(参见 [ADR-0011](docs/arch/adr/0011-slash-only-reveal-with-oob-for-high-sensitivity.md))。 | 标志 | 设置者 | 通道 | 适用条件 | |:---|:---|:---|:---| | `slash_command` | Claude Code **客户端** | MCP 会话上下文 —— *非*工具参数 | **所有**敏感度级别 | | `oob_ack` | **OOB 通知器** | 独立的 OS 通道(桌面通知 / TTY 提示 / `localhost` 确认) | `sensitivity=high`(**与** `slash_command` 同时满足) | 在此基础之上,`vault_reveal` 和 `vault_delete` 还会查询第三个独立信号:在 MCP `_meta` 字段中,由 key `dev.farchanjo.merkle/operator_confirmation` (MERK-001) 携带的**操作员确认**。LLM 只能控制工具 `arguments` 对象;`_meta` 是由客户端传输层写入的。`VaultRevealInput` 和 `VaultDeleteInput` 故意**没有** `operator_confirmation` 字段——因此试图通过参数设置它的模型将毫无作用。 ### 高敏感度 reveal 的端到端流程 ``` sequenceDiagram actor Operator participant CC as Claude Code Client participant LLM as LLM Context participant MCP as merkle-mcp (stdio) participant Agent as merkle-agent (daemon) participant OOB as OOB Notifier Operator->>CC: types /merkle-reveal {handle} "reason" CC->>LLM: injects slash_command + operator_confirmation (_meta) LLM->>MCP: vault_reveal { handle, purpose } Note over LLM,MCP: no operator_confirmation in arguments MCP->>Agent: POST /v1/reveal (slash_command=true) Agent->>Agent: verify slash_command + sensitivity=high Agent->>OOB: issue OobChallenge (id, nonce, expires_at) OOB->>Operator: desktop notification / TTY / localhost page Operator->>OOB: Approve -> Ed25519 sign(id, nonce) OOB->>Agent: OobResolution { oob_ack=true, signature } Agent->>Agent: verify Ed25519 + nonce binding + rate limit Agent->>Agent: append audit entry (pre-commit) Agent->>MCP: plaintext MCP->>LLM: tool result ``` 对于低/中等敏感度,OOB 步骤将被跳过;仅需要 `slash_command`(和操作员确认)。如果需要 OOB 但尚未确认,agent 会返回 `{ "oob_pending": true, "oob_channel": ..., "expires_at": ..., "request_nonce": ... }`,调用方在确认后重新发起请求。 ## 封存与解封生命周期 在冷启动时,保险库处于**封存** 状态——Vault Root Key (VRK) 不在内存中,所有的读写操作都会被拒绝。解封会加载并解密 VRK;封存则会将其清零。 ``` stateDiagram-v2 [*] --> Sealed : vault init / cold start Sealed --> Unsealing : merkle unseal (begin write-lock) Unsealing --> Sealed : any failure (keychain / AEAD / HMAC) — rollback Unsealing --> Unsealed : HMAC key published + state flip + audit op=Unseal Unsealed --> ShuttingDown : merkle seal / SIGTERM ShuttingDown --> Sealed : drain complete (30s hard timeout) note right of Unsealing nonce(24) || ciphertext AEAD-decrypted AAD = b"vault-root-key" HMAC key published BEFORE state flip Direct Sealed → Unsealed is REJECTED end note ``` **解封操作顺序:** 获取写锁 → 从 keychain 读取 `vrk-master-v1` blob → AEAD 解密它 (AAD `b"vault-root-key"`) → 派生审计 HMAC key `BLAKE3_keyed(vrk_bytes, b"merkle vault hmac key v1")` → **在**状态切换为 `Unsealed` 之前**发布 HMAC key** → 追加一条 `op=Unseal` 记录。在读取与发布之间发生的任何失败都会回滚到 `Sealed` 状态。 ## 审计哈希链 对密钥的每一项操作都会被记录在只能追加、防篡改的链中——将 Ralph Merkle 的哈希链应用于你的审计日志。 ``` flowchart LR G["Genesis
prev_hash = blake3:0000…0000
hmac = keyed(hash ‖ id)"] E1["Entry 1
hash = BLAKE3(canonical ‖ prev)
hmac = keyed(hash ‖ id)"] E2["Entry 2
hash = BLAKE3(canonical ‖ E1.hash)
hmac = keyed(hash ‖ id)"] PH[("PinnedHead
head_hash ‖ head_seq ‖ entry_count
MAC under same key")] G --> E1 --> E2 --> PH ``` - **无密钥哈希:** `current_hash = BLAKE3(canonical_fields || prev_hash)` —— 编辑任何条目都会导致后续所有哈希值产生分歧。 - **带密钥 HMAC:** `hmac = BLAKE3_keyed(hmac_key, current_hash || id_uuid)` —— 密钥在解封时从 VRK 字节派生,且绝不单独持久化存储。 - **PinnedHead**(一行单独的 SQLite 记录)将链尖端绑定在相同的密钥下,挫败*截断后重建*攻击。 - `ChainVerifier::verify_full` 会报告精确的拒绝结果:`GenesisAnchorMissing`、`BrokenAtEntry`、`HmacMismatch`、`MissingHmac`、`HeadMacMismatch`、`TruncationDetected`、`HeadHashMismatch` 等。 ## 加密与安全 ### 基础组件 | 组件 | Crate | 用途 | |:---|:---|:---| | XChaCha20-Poly1305 | `chacha20poly1305` | 每个 blob 的 AEAD 加密 (24 字节 nonce,16 字节 tag) | | BLAKE3 hash + keyed | `blake3` | 审计哈希链完整性,HMAC 替代品,KDF (XOF) | | Argon2id (RFC 9106) | `argon2` | 密码 KDF —— 最低限制 `m_cost≥65536`,`t_cost≥3`,`p_cost≥1` | | age / X25519 | `age` 0.11 | 双接收者备份加密 + 文件密钥库 (scrypt `log_n` 固定为 18) | | Ed25519 | `ed25519-dalek` | OOB 解析签名,操作员证明 JWTs | | X25519 ECIES | `x25519-dalek` | OOB 挑战载荷加密 ([ADR-0019](docs/arch/adr/0019-ecies-encryption-for-oob-challenge-payload.md)) | | OsRng | `rand` 0.10 | 所有密钥/nonce 生成 —— 熵门控 `assert_entropy_gate()` | 不使用 OpenSSL。所有基础组件均为纯 Rust 实现(`deny.toml` 中禁止使用 `openssl`/`openssl-sys`/`git2`)。 ### 关键保证 - **Zeroize:** `MasterKey`、`VaultRootKey`、`NamespaceDek` 和 `PrivateBlob` 在 drop 时会被清零;`Debug` 被重写为输出 `[REDACTED]`;`MasterKey::clone()` 故意**不**复制密钥字节。 - **SSRF / DNS-rebind 防御:** `DestinationPolicy::strict()` 拒绝非 HTTPS、loopback、link-local(包括 `169.254.x.x` 云元数据 IP)、private、CGNAT、multicast 和 IPv6 ULA/link-local —— *在*附加凭证*之前*。`ValidatingDnsResolver` 在连接时再次应用相同的检查,以消除 TOCTOU 漏洞,并 fail closed。 - **对端凭证 socket 认证 (GAP-007):** 父目录强制为 `0700`,绑定期间 umask 为 `0177`,显式对 socket 执行 `chmod 0600`。macOS 使用 `LOCAL_PEERCRED`,Linux 使用 `SO_PEERCRED` + `/proc/
基础与领域
| Crate | 职责 | |---|---| | `merkle-types` | 共享值对象 (`Handle`、`Sensitivity`、`UuidV7`、`Blake3Hash`、`AuditOp`、……)。零基础设施依赖。 | | `merkle-domain-identity` | 身份与封存 —— `VaultIdentity`、`MasterKey`、`VaultRootKey`、`SealedState`、`UnsealProtocol` | | `merkle-domain-secret-storage` | 密钥存储 —— `Secret`、`SecretVersion`、`Namespace`、`PrivateBlob`、`RetentionPolicy` | | `merkle-domain-access-mediation` | 访问中介 —— `RevealRequest`、`UseToken`、`Tempfile`、`Fifo`、`OobChallenge`、`decision::evaluate` | | `merkle-domain-audit-compliance` | 审计与合规 —— `AuditEntry`、`AuditLog`、`PinnedHead`、`ChainVerifier` | | `merkle-domain-backup-recovery` | 备份与恢复 —— `Backup`、`BackupScheduler`、`RestorePlanner`、`AnacronState` | | `merkle-domain-policy-permissions` | 策略与权限 —— `NamespacePolicy`、`RevealPolicy`、`RateLimit`、`PolicyEvaluator` |Ports、adapters、客户端、应用与二进制文件
| Crate | 职责 | |---|---| | `merkle-ports` | 纯 trait 定义:`StoragePort`、`KeychainPort`、`CryptoPort`、`OobNotifierPort`、`ExternalServicesPort` | | `merkle-adapter-sqlite` | sqlx + SQLite WAL;每个 blob 的 AEAD,FTS5,只追加的审计触发器,`pinned_head` | | `merkle-adapter-keychain` | 跨平台 OS keychain (`keyring`,服务名 `dev.farchanjo.merkle`);基于文件的 `age` 密钥库回退 | | `merkle-adapter-crypto` | `RustCryptoAdapter`:XChaCha20-Poly1305、BLAKE3、Argon2id、age、Ed25519、X25519 ECIES | | `merkle-adapter-oob` | OOB 确认(桌面 / TTY / localhost);验证 Ed25519 `OobResolution` | | `merkle-adapter-external-services` | SSH exec + HTTP (reqwest/rustls),内置 SSRF/DNS-rebind 防护 | | `merkle-adapter-companion-socket` | 基于 Unix socket 的 axum HTTP/1.1 —— 唯一的入站端口。⚠️ 唯一将 `unsafe_code` 放宽为 `deny` 且 `missing_docs` 放宽为 `allow` 的 crate | | `merkle-adapter-mcp` | rmcp MCP server:30 个工具 + 4 个 prompts。依赖于 `merkle-companion-client`,**而非**领域层 | | `merkle-companion-client` | 可复用的 HTTP/1.1-over-Unix-socket 客户端 (hyper + 自定义 `UnixConnector`) | | `merkle-application` | 用例编排:`AppContext`、命令 + 查询处理器。不导入任何基础设施 | | `merkle-bdd` | Cucumber 验收测试套件;加载 15 个 `.feature` 文件 | | `merkle-e2e` | 黑盒 E2E;生成 `merkle-agent`;全部标记为 `#[ignore]` | | `bin/merkle-agent` → `merkle-agent` | Daemon / 组合根 | | `bin/merkle-cli` → `merkle` | 操作员 CLI | | `bin/merkle-mcp` → `merkle-mcp` | 瘦 stdio MCP server |标签:MCP, Rust, 哈希链审计, 域名驱动设计, 本地优先, 网络流量审计, 通知系统