OpenProgram · 历史设计归档 · 2026-08-13
已禁用的文件凭据加固设计
本文保留旧方案及实施记录,仅供追溯,不再作为当前实现要求。
当前决定:凭据继续使用普通本地文件读写。0600/0700 强制、owner/type/symlink/inode 检查、权限审计与修复和 revision conflict 均已禁用。只保留避免并发丢写所需的普通写锁。除非用户以后单独明确提出,不得重新启用旧加固方案。
openprogram/credential_files/safety/legacy.py。当前生产代码不导入、不使用;除非用户以后单独明确提出,否则保持停用。0集成实现:33dd7d2c
以下是当前集成树的实现状态。后续第 1–5 节原样保留 2026-08-11 已批准的基线、参考 corpus、设计决策与验收模型;其中“当前行为”“后续计划”“将来”是批准设计时措辞,不覆盖本节的集成状态。
清单与文件发布
openprogram/credential_files.py 的 SECRET_INVENTORY 注册秘密路径/字段、生命周期和备份策略。共享 private writer/update 实现唯一私有临时文件、稳定锁、revision conflict、同目录 replace、文件与 POSIX 目录 fsync、symlink/owner/type 检查及 committed_not_durable。
写入消费者
config.json、profile .env、AuthStore、Channel access/credentials、MCP config/token 和 Web token 已迁移到共享契约;集成修复继续收紧文件系统竞态与 inventory wildcard 的 segment 边界。
审计、编辑与删除
openprogram doctor credentials [--repair] 以 inventory 审计并只修复当前用户拥有的普通 POSIX 文件/目录;API/Web/CLI 使用 omit/replace/delete 语义,拒绝 display mask/redaction sentinel,旧 reveal 路由固定返回 410,删除失败不报告成功。
备份与恢复
默认备份按字段移除注册秘密,--include-credentials 只包含允许的长期秘密,runtime/pairing token 永不进入。恢复先 staging 与验证,再以 durable journal 发布;失败执行 rollback,启动恢复会拒绝不安全或不可恢复的 journal。
0600 明文 inventory 设计,不是加密存储。Windows native 文件权限、ACL、恢复和并发行为没有作为本次完成条件;OS Keychain、Python keyring 与 Windows Credential Manager 不是待补项,而是明确排除项。1批准设计时基线:秘密清单和证据边界
本节保留 2026-08-11 批准设计时基线。清单按当时写入代码逐项核对,而不是按目录名推断。混合文件只把明确字段视为秘密;临时令牌也进入清单,但备份策略与长期凭据不同。当前实施状态以第 0 节为准。
| 文件/字段 | 秘密性质 | 当前写入保障 | 已确认缺口 |
|---|---|---|---|
auth/<provider>/<account>.jsonprofiles/<name>/auth/… | AuthStore pool:API key、OAuth、现有 credential_process 配置等认证记录 | 较完整 0600-at-create、文件 fsync、replace、restrict_to_user、稳定 lock、外部修改检测 | 固定 .tmp;无 O_NOFOLLOW;无目录 fsync;不同 writer 尚未共用同一契约 |
config.json[api_keys] | 混合配置中的 provider API keys | 最终文件以 0600 打开;更新路径有进程内锁和可用时的 FileLock | 直接 O_TRUNC 最终文件;无临时文件、fsync、replace、权限验证;“atomic”注释不符合崩溃语义 |
profiles/<name>/.env | Account 环境变量凭据 | 同目录普通临时文件后 replace | 临时文件创建时不是 0600;无 fsync、restrict_to_user、符号链接防护或跨进程锁 |
channels/<channel>/accounts/<id>/credentials.json | Channel account credentials | replace 后调用 restrict_to_user;进程内 RLock | 创建先 write_text 后收紧;固定 .tmp;无 0600-at-create、fsync、O_NOFOLLOW 或跨进程锁 |
channels/<channel>/access.json 的 pending pairing code | 短期认证材料;同文件 allowlist 是元数据 | mkstemp 私有创建、chmod、replace、FileLock | 无文件/目录 fsync、最终权限验证;临时 pairing code 不应进入备份 |
mcp_servers.json 的 env、headers、bearer、OAuth client secret | 混合配置中的秘密;任意 env/header 值均按秘密处理 | 较完整 0600-at-create、O_NOFOLLOW、文件 fsync、replace、restrict_to_user | 固定临时名;无目录 fsync;读改写没有统一跨进程锁 |
mcp_tokens/<server>.json | access/refresh token 及客户端认证信息 | 0600、O_NOFOLLOW、replace、restrict_to_user | 写入未 flush+fsync;固定临时名;无目录 fsync或跨进程锁 |
web/token | Web 运行期 bearer token | 较完整 随机同目录临时名、O_EXCL/O_NOFOLLOW、0600、文件 fsync、replace、权限限制、回读验证、生命周期锁 | 无目录 fsync;属于 never_backup,关闭时仅逻辑删除 |
已核对为非秘密的状态
channels/…/account.json 是账号元数据;owner.json 是稳定 principal ID;WeChat cursor.json 是运行进度游标;web/access.json 记录 bind/origin/fingerprint。它们可以继续采用私有权限,但不进入“原始秘密不可返回”和 credential backup 的清单。若字段含义以后改变,必须先更新清单再写入。
目录和跨平台基础
ensure_state_dir 已在 POSIX 将 profile root 收紧为 0700,这是已有纵深保障,不能代替每个文件的创建时权限与验证。restrict_to_user 当前在 POSIX 使用 chmod 0600,在 Windows 以固定 argv 调用 icacls 移除继承并授权当前用户与 SYSTEM,但它是 best-effort 并吞掉失败;目标实现必须验证结果并把失败返回给调用方。
2批准设计时的当前行为与目标行为
2.1 用户录入或导入秘密
当前行为与风险
setup、账号管理、Channel、MCP API/CLI 各自解析输入。Web config 和 Account 接口已拒绝把显示用 mask 当成新 key;MCP 的合并逻辑能处理结构化 masked 响应,但提取出的纯 mask 字符串仍可能被作为新值写回。导入/恢复与交互录入没有共用秘密字段清单。
需要完善与完成后行为
所有入口使用三态更新:字段省略表示保留,显式 delete 表示删除,只有通过格式验证的新明文表示替换。任何 mask、presence marker 或 redacted sentinel 均拒绝写入。导入先按清单分类和验证,再进入同一 writer。
2.2 OpenProgram 保存
当前行为与风险
AuthStore、MCP config、Web token 已有多数原子写保障;config.json 可能在崩溃时截断,Account/Channel 临时文件可能以宽权限出现,MCP token 可能 replace 尚未落盘的数据。固定临时名还扩大了并发和符号链接风险。
需要完善与完成后行为
所有清单内 writer 只调用共享 private atomic writer;先写同目录唯一私有临时文件,fsync 后原子 replace,再持久化目录项并验证 owner-only 权限。读改写在稳定锁下执行,冲突不静默覆盖。
2.3 运行时解析与使用
当前行为与风险
运行时从多个文件解析 provider、Account、MCP 和 Channel 凭据。既有 credential_process 只按 argv 执行、shell=False,有超时、解析规则和进程内 cache;这是现有明确能力,不是通用 secret backend。
需要完善与完成后行为
读取前验证路径类型、owner 和权限;拒绝 state root 之下的符号链接、非普通文件和外来 owner。读取失败只报告凭据类别、路径和修复动作,不记录值。设计不增加外部取密钥命令,也不扩大 credential_process 契约。
2.4 用户查看、编辑与删除
当前行为与风险
Web config、Auth CLI 和 MCP 的常规列表响应多数只返回 has_value 与 mask;但账号管理仍有按账号返回完整 API key 的 reveal 端点和界面按钮,mcp edit 也会把原始 mcp_servers.json 交给 $EDITOR。Channel 删除使用 rmtree(ignore_errors=True),可能把失败显示为成功。
需要完善与完成后行为
移除账号 key reveal 的 API、Web 与 CLI 入口;旧 reveal 路由返回稳定的弃用错误,不返回原值。MCP 改为结构化 masked 编辑,不再由产品打开原始秘密文件。其他 API/Web/CLI 同样不得返回原始秘密。删除在锁内执行、清理运行时 cache、验证文件不存在并明确返回失败;不声称对 SSD、日志结构文件或备份执行安全擦除。
2.5 备份、恢复与迁移
INCLUDED 包含带 api_keys 的 config.json、带 credentials.json 的 channels/、带 env/header/bearer/OAuth secret 的 mcp_servers.json;CREDENTIAL_ENTRIES 却只包含 auth 与 mcp_tokens。因此未传 --include-credentials 时虽然打印 “credentials excluded”,默认归档仍可能含秘密。profile AuthStore 和 .env 又不在完整包含清单中。当前备份不能描述为已经安全。当前行为与风险
backup create 有显式 --include-credentials 和警告,但分类依赖顶层名称;归档是创建后 chmod,缺少 0600-at-create、fsync 与目录 fsync。restore 校验路径并拒绝符号链接,但使用 tar.extractall 后没有按秘密清单重新限制和验证,也没有多文件崩溃事务。顶部“迁移到 system keychain”注释已经过时。
需要完善与完成后行为
保留显式 opt-in 产品语义,不自动改成默认包含凭据。默认备份按字段清除 api_keys、MCP secret 和 Channel credential,排除 profile .env/AuthStore 及 pending pairing code;恢复缺失或 redacted 字段时保留本机现有秘密。只有明确 --include-credentials 和警告后才包含清单允许的长期秘密;web/token 与 pairing code 永不备份。
恢复先在 state parent 下的同一文件系统 staging 目录验证 manifest、成员路径、文件类型、JSON 结构和 secret inventory;拒绝 symlink、hardlink、未知秘密路径和越界成员。每个目标以共享 writer 发布,并记录恢复 journal。发布前失败不改变旧状态;中途失败按 journal 反向恢复旧文件。权限修复和最终验证全部成功后才报告完成。默认 pre-restore 备份不擅自获得凭据权限;执行带凭据恢复时,用户的同一显式授权允许 pre-restore snapshot 包含凭据,并再次显示明文备份警告。
2.6 诊断与故障恢复
当前行为与风险
写入路径各自补 chmod/ACL,缺少一个能发现历史 0644 文件、外来 owner、symlink、残留临时文件和不完整 restore 的清单驱动诊断。Windows ACL 失败目前可能被吞掉。
需要完善与完成后行为
openprogram doctor credentials 只输出类别、路径和状态;--repair 只修复当前用户拥有的普通文件/目录,不跟随 symlink,不接管其他 owner。无法验证 owner-only 时返回非零。残留临时文件只在取得对应锁且满足命名和年龄规则时删除。
3其他项目怎么设计
对比只使用官方文档/源码或仓库已有 reference corpus。外部项目均按本文日期可见文档或本仓固定源码快照描述,不代表最新版本,也不把文档声明等同于端到端运行验证。完整版本边界见 feature matrix 的方法与证据。
| 项目 | 可确认设计 | 对本设计的使用方式 | 证据范围 |
|---|---|---|---|
| Claude Code | 官方文档声明凭据安全存储;gateway 文档提供有 TTL 和明确优先级的 apiKeyHelper。 | 采用明确优先级、缓存边界和不回显原则;不因其系统存储选择而采用 Keychain。 | 官方 getting started;官方 gateway 文档;本仓快照矩阵 |
| Codex | 本仓固定快照记录可选 keyring store;具体能力受版本与配置限制。 | 仅作为竞品事实保留;OpenProgram 明确拒绝该 backend,采用文件契约与审计。 | 本仓 reference corpus 方法表;官方配置参考 |
| OpenClaw | SecretRef 支持 env/file/exec,启用与解析有 preflight/audit;plaintext 兼容仍存在,OAuth/运行时生成材料不都归入只读 ref。 | 修改采用其“凭据面清单驱动验证/审计”思路;拒绝增加 env/file/exec backend。 | 官方 credential surface;官方 secrets 文档 |
| OpenCode | 官方 provider 文档明确本地凭据文件 ~/.local/share/opencode/auth.json。 | 说明本地文件方案是可独立成立的产品选择;不据此推断其原子写、权限修复或备份成熟度。 | 官方 provider 文档;本仓固定源码快照 |
| Hermes | 本仓源码引用显示其 auth 文件路径采用 advisory lock、版本字段与 lock timeout;另有 backup/import 能力。 | 采用稳定锁和版本冲突思路;备份安全仍按 OpenProgram 自身 secret inventory 验收。 | 官方源码仓库;本仓固定源码快照 |
| CodeBuddy | 本仓 npm 快照记录系统凭据存储与多种凭据入口。 | 只保留竞品事实;不把支持 Keychain 当成 OpenProgram 必须采用的理由。 | 本仓 feature matrix 记录的 codebuddy 2.109.3 包快照 |
| pi-mono | 本仓固定快照仅提供部分相关证据,不足以确认完整凭据持久化、权限和备份契约。 | 不纳入设计依据,不对成熟度作推断。 | 本仓固定源码快照 |
4我们后续怎么计划
4.1 采用、修改、拒绝
采用 文件写入基本契约
owner-only 创建、同目录原子 replace、文件 fsync、目录 fsync、稳定 lock、版本冲突、清单驱动审计、秘密不回显。
修改 清单和备份设计
SecretRef 类项目的 credential surface 思路改为 OpenProgram 固定文件/字段清单;清单同时驱动 writer、backup、restore、doctor 和测试。
拒绝 新 secret backend
不采用 Keychain、keyring、加密数据库;不新增通用 env/file/exec 密钥解析或任意 shell 命令。现有 credential_process 不扩张。
4.2 单一 secret inventory
引入静态、可测试的注册表。每条记录至少包含路径 matcher、秘密字段 matcher、writer、生命周期(persistent/ephemeral)、备份策略(redact_default、include_on_opt_in、never_backup)和删除动作。任何新持久化秘密若未注册,测试直接失败;备份不得再只看顶层目录。
4.3 private atomic writer 的机械契约
- 路径:先解析并验证 state root。允许用户显式配置的 state root 自身是指向当前用户拥有、owner-only 目录的别名;root 以下任何路径分量、最终文件或临时文件为 symlink 时均拒绝。使用
lstat,只接受当前用户拥有的普通文件/目录。 - 目录:POSIX 秘密目录必须是
0700;Windows 必须移除继承并只授权当前用户与 SYSTEM。创建前后都验证,不以父目录权限替代文件权限。 - 临时文件:在目标同一目录用不可预测唯一名称和
O_CREAT|O_EXCL|O_NOFOLLOW创建,POSIX mode 为0600;Windows 创建后立即应用并验证 owner-only ACL。禁止固定.tmp。 - 写入:完成序列化后写入全部字节,flush 并 fsync 临时文件;在发布前再次验证 owner 和权限。
- 发布:只以同目录
os.replace发布。因此临时文件与目标天然同一文件系统,不会发生EXDEV;如果实现偏离而出现跨文件系统错误,直接失败,禁止回退为 copy。 - 目录持久化:POSIX 对父目录执行 fsync,保证 rename 的目录项持久化。Windows 的 Python 路径没有可移植目录 fsync,验收标准是文件 flush/fsync、原子 Replace/Move 语义和 ACL 回读验证,并明确记录该平台差异。
- 最终验证:replace 后再次限制并验证最终文件。权限验证失败必须返回错误,不能记录成功;临时文件在 finally 中按已验证路径清理。
- 失败语义:replace 前失败保持旧文件;replace 失败保持旧文件;replace 成功而目录 fsync 失败时新文件已可见,返回结构化
committed_not_durable,不再以旧内容反向覆盖。
4.4 并发和外部编辑
每个逻辑记录使用 owner-only 稳定 sibling lock,进程内锁与跨进程 lock 同时保留,锁覆盖完整 read-modify-write。API/Web 更新携带 revision/ETag;读取时计算版本指纹。手工编辑器不持锁,因此写入前发现版本变化就返回 conflict,不静默覆盖。固定临时文件名全部移除。
4.5 权限审计与修复
- 启动时可做低成本抽样;完整扫描由
openprogram doctor credentials执行,输出不含值。 - POSIX 修复当前用户拥有的普通文件为 0600、秘密目录为 0700;Windows 初期沿用现有固定 argv 的
icacls调用,不经过 shell,并解析和回读验证 ACL。 - symlink、非普通文件、外来 owner 和 ACL 无法验证均不自动接管,返回非零并给出路径级动作。
- 历史宽权限文件在成功修复前不得被运行时读取;这会使旧安装显式失败,但不会继续在错误权限下使用秘密。
4.6 错误、删除和日志边界
错误类型区分 permission、ownership、symlink、conflict、serialization、replace、fsync 和 committed_not_durable。日志、异常、遥测和 doctor 输出只包含 credential kind、相对路径、operation、errno/ACL 状态,不包含原始值、命令 stdout 或完整 header/env。删除是逻辑 unlink/rmtree 后验证,不承诺介质安全擦除;备份中的旧值直到用户 prune 或保留期结束仍可能存在。
4.7 兼容与实施批次
| 批次 | 内容 | 完成条件 |
|---|---|---|
| 批次 0 | 先建立 inventory,并把 private atomic writer 的最小实现用于备份归档本身;随后修正默认排除/字段 redaction、过时 Keychain 注释和恢复后的权限验证。备份归档必须从首次可见起即为 owner-only,不能等批次 1 再修。 | 默认归档扫描不到任何注册秘密;opt-in 归档精确覆盖允许备份的长期秘密;归档创建权限、fsync/replace 与警告、manifest 一致。 |
| 批次 1 | 实现共享 private atomic writer;迁移 config、Account .env、Channel、MCP token;再让 AuthStore、MCP config、Web token 使用同一 primitive。 | 所有 writer 通过相同 POSIX/Windows 契约测试,无固定 tmp 或直接截断最终秘密文件。 |
| 批次 2 | secret inventory、doctor/repair、masked 三态编辑、删除结果验证;移除产品内原始 MCP 文件编辑入口。 | 历史权限、符号链接、mask 回写、原始秘密响应和删除失败均有负向测试。 |
| 批次 3 | staged restore、journal/rollback、完整 manifest、跨平台 backup/restore 验收。 | 注入各故障点后旧状态或新状态之一完整成立,不出现静默部分成功。 |
4.8 测试矩阵
| 维度 | 必须覆盖的用例 | 机械判定 |
|---|---|---|
| 创建与权限 | umask 000/022;首次创建;覆盖 0644 历史文件;0700 目录;Windows ACL 继承开启/关闭 | 秘密从首次可见起即 owner-only;最终权限回读通过 |
| 崩溃持久化 | 写入中、文件 fsync 前后、replace 前后、目录 fsync 失败 | 旧/新完整值;无截断 JSON;目录 fsync 失败返回 committed_not_durable |
| 并发 | 线程、两个进程、手工外部修改、锁超时 | 无丢失更新;冲突或超时明确返回 |
| 路径攻击 | 目标 symlink、任一路径分量 symlink、预置固定 tmp、非普通文件、外来 owner | 拒绝且不修改链接目标;实现没有固定 tmp |
| 文件系统 | 同目录 replace;模拟 EXDEV;POSIX 目录 fsync;Windows Replace/Move + ACL 回读 | 不发生 copy fallback;平台差异符合契约 |
| 显示与编辑 | API/Web/CLI GET;mask 原样提交;省略/替换/删除;MCP structured edit | 响应无原文;mask 不可写入;三态一致 |
| 备份 | 默认、--include-credentials、mixed JSON、profile、Channel、MCP、runtime token | 默认零原始秘密;opt-in 只含允许项;runtime/pairing token 永不进入;manifest 与提示一致 |
| 恢复与删除 | 恶意 tar、权限过宽、symlink、字段缺失、恢复中崩溃、删除失败、旧备份残留 | 验证失败不发布;缺失秘密保留;journal 可恢复;删除不虚报;不声称安全擦除 |
5feature matrix 实心圆 gate
“凭据存操作系统钥匙串”继续保持 OpenProgram ·,并标为评估后拒绝/非计划项。它不是文件加固的验收代理。当前矩阵不新增“文件凭据持久化一致性”能力行,避免把设计状态误计为实现状态。
- 所有由 OpenProgram 持久化的秘密文件/字段均注册在 inventory,且无未登记 writer;
- 全部 writer 在 POSIX 和 Windows 通过共享创建权限、原子写、fsync/replace、ACL/owner 验证、并发与 symlink 测试;
- doctor/repair 能发现并按边界修复历史权限,拒绝 symlink 与外来 owner;
- 默认备份含零原始秘密,opt-in 与 inventory 精确一致,恢复会验证并重新限制权限;
- API/Web/CLI 不返回原始秘密,mask/sentinel 不可作为新秘密写入;
- 崩溃、目录 fsync、冲突、删除和 restore journal 的失败语义全部有自动化测试。
满足后新增行时,必须同时重算功能总数、OpenProgram 得分、候选数、旧/新增差异数和页面叙述。
6集成实施与验证证据
集成候选:33dd7d2c。credential 实施由 81d01e8d(private 文件存储)、a953c046(审计与秘密维护)、9bd4b00d(staged restore)及其后续修复组成;集成审查修复包括 76427af8、4403ee2b、a40e0773、ea3637a2、aa855c4c、eb615416、05874c33、d561757a、79a0df8f、32533639。最终 fixture/integration seam 修复止于 33dd7d2c。
| 门禁/阶段 | fresh 结果 | 证据解释 |
|---|---|---|
| Specification review | PASS · 0 | F1–F7 修复后,分组 affected:eb615416 238 passed;ea3637a2 151 passed、1 skipped;aa855c4c 82 passed;05874c33 83 passed。 |
| Quality review | PASS · 0 | 修复分组:d561757a 154 passed;79a0df8f 133 passed;32533639 184 passed。最终 quality 原始 reproduction 16 passed,direct affected 137 passed。 |
| Whole-branch review | PASS · 0 | final whole review 检查 122 个 first-failure files;无 remaining finding。 |
| 完整 Python gate | 5265 passed, 10 skipped, 2 deselected, 1 xfailed | 在集成候选上 fresh 执行。上述 affected 数值是不同审查/修复批次的分组证据,不声称存在一个未执行的单一 affected union。 |
合入 main 后最终 gate(0266a046) | 5286 passed, 11 skipped, 2 deselected, 1 xfailed | 与最新 main 双向合并后 fresh 执行;合并后独立 SPEC 与 QUALITY 复审均 PASS,credential 加固的权限 fail-closed、O_EXCL/inode 校验与 restore 回滚 journal 在合并结果中逐项核验在位。 |
| 文档 gate | 490 pages;0 broken links | 集成候选文档构建与链接检查结果。 |
实现定位:openprogram/credential_files.py;openprogram/_cli_cmds/backup.py;openprogram/_cli_cmds/doctor.py;openprogram/auth/store.py;openprogram/auth/accounts.py;openprogram/setup.py;openprogram/channels/accounts.py;openprogram/channels/_access.py;openprogram/mcp/config.py;openprogram/mcp/token_storage.py;openprogram/webui/owner_auth.py;openprogram/webui/routes/_credential_secrets.py;openprogram/webui/routes/accounts.py;openprogram/webui/routes/mcp.py。主要验收文件为 tests/unit/test_credential_files.py、test_credential_writer_hardening.py、test_credential_doctor.py、test_credential_edit_semantics.py、test_backup_command.py、test_backup_staged_restore.py 与 test_secret_non_retrievability.py。
6.1 POSIX 实施证据
已实现并由自动化回归覆盖:注册秘密的运行时 reader 在权限不是 0600 时拒绝读取,直到 doctor credentials --repair 完成;公共 credential writer 在共享 private primitive 验证路径前不创建父目录;doctor 会报告注册秘密上级目录的 symlink;restore journal 使用同目录唯一临时文件、no-follow/O_EXCL、短写循环、文件与目录 fsync 及原子 replace,恢复读取不跟随 symlink;restore 会拒绝未获 manifest opt-in 授权的实际秘密成员,并在保存旧目标前拒绝 symlink;MCP 首次 restart 失败只返回稳定结构化状态,原始异常不进入响应或异常链。对应公共回归位于 test_credential_writer_hardening.py、test_credential_doctor.py、test_backup_staged_restore.py、test_auth_store.py 和 test_mcp_secret_non_retrievability.py。
规格复审补充的边界均已实施:restore journal 在回滚前严格验证版本、字段类型和 state/backup containment;manifest 只接受版本 1、布尔 credential_opt_in;restore payload 先写入 state 同一文件系统上的 0700 staging directory 和 0600 文件,再进入 journalled publish;Channel access/pairing 路径不预先创建目录;doctor 可识别 inventory 通配层中的中间 symlink;AuthStore pool 与 MCP token 删除均在 sibling lock 下 no-follow 校验 inode、owner、0600 与删除结果,失败时不报告成功。
恢复路径验证在任何回滚修改前一次性完成:逐级 lstat target 与 previous 的全部现存组件,拒绝 symlink、外来 owner 和错误类型。Journal 的 relative path 与 previous 必须唯一,previous 使用与 entry 序号绑定的无碰撞名称;重复 tar member、重复 journal entry 或非法路径均在 state 内外零修改时拒绝。中途发布失败的多文件回滚测试覆盖旧扁平编码会发生碰撞的两条路径。
路径验证和修改之间不回到完整路径:恢复会逐级以 O_DIRECTORY|O_NOFOLLOW 打开并持有 target parent directory fd,同时持有已验证 previous source fd;恢复旧文件时从 source fd 复制到 target directory fd 下的 owner-only 唯一临时文件,再用带 src_dir_fd/dst_dir_fd 的 replace 发布;删除新增文件时使用带 dir_fd 的 unlink。父目录或 source 名称在验证后被替换的竞态回归确认不会写入或删除 state root 外文件。
存在但不可解析、schema 非法或路径不可安全恢复的旧 journal 与“没有 journal”使用不同结果:前者抛出 UnrecoverableRestoreJournalError 并阻止任何新 restore。新 restore 拒绝复用残留 .restore-journal.d;旧内容保存从逐级 no-follow 打开的 source fd 复制到已绑定 backup directory fd 下以 O_CREAT|O_EXCL|O_NOFOLLOW 创建的 0600 文件,不使用 path-based copy2 或事后 chmod。
Restore 在读取旧 journal 前获取 state 级稳定 owner-only sibling lock,并持有到成功 finish 或失败 rollback/cleanup 完成;同一个锁也覆盖 CLI safety snapshot 到 restore finish 的完整区间,普通 create_backup 作为一致性 reader 在并发 restore 期间返回 busy,不读取部分发布状态。CLI 只在 journal 已清理、rollback 可确认完成时声明 previous state restored,unrecoverable 与 busy 分别输出稳定失败状态,busy 不先创建 safety snapshot。MCP registry 及 Web list/detail/start/stop/restart/test 的公开错误只使用稳定 code/kind;MCP client 内部状态、await-session 异常和 transient stderr 不保存或拼接原始异常文本,restart 的递归异常链同样不保留原异常。