Draft the U1 identity manifest contract, data ownership inventory and card

Why: U1 starts per the approved recovery plan r2 - a reviewed design contract must exist before touching scripts/svc.sh, and R3 needs an enumerable data-ownership baseline.

Evidence: SELF - doc-check receipt via make evidence. First run FAILED (exit 1: proposal r2 not present in this worktree because it sits unmerged on feature/service-restart-recovery-plan); references reworded to cite the path with a pending-merge note and the check re-run PASS (exit 0). Both receipts are in the card; grounding facts verified at baseline 89174a0.

Impact: docs/recovery/ additions and task card only; no behavior change; implementation gated on design review.

Refs: modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md r2 U1
This commit is contained in:
2026-10-12 01:39:20 +00:00
parent 39f7c08af6
commit c43ccd7481
3 changed files with 210 additions and 12 deletions
File diff suppressed because one or more lines are too long
+26
View File
@@ -0,0 +1,26 @@
# U1 数据归属清单 v0.1(R3"所有数据"的枚举基线)
状态:设计候选,随 U1 评审;配套设计见 [u1-identity-manifest.md](u1-identity-manifest.md)。主记录:方案 r2 第 3 节 R3 与第 6 节(提案在 feature/service-restart-recovery-plan 分支待合入,路径 `modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md`)。标注规则:`已核实`=SELF 源码观察(基线 Dev `89174a0` / Backend 同指针);`待核对`=实施 I2 时必须落定,未落定不得进入备份/恢复范围声明。
本清单只回答"数据在哪里、谁是权威、怎么恢复";耐久性参数(fsync/WAL 同步档)与备份窗口属 U6。
| # | 数据类别 | 权威位置 | 存储/机制 | 恢复方法 | 状态 |
|---|---|---|---|---|---|
| 1 | 团队业务库(账号/项目/成员/消息/文档/卡片/Surface 修订/设置/插件授权/run/step/命令/通知/审批/取消/额度/恢复 hold/事件) | PG 团队库(dbref 指向实例+库) | `modules/backend/internal/store/migrate.go` 表族(v17 pending_notices、run_commands、surface_revisions、recovery_holds、execution 等已核实存在) | PITR/pg_dump 逻辑备份(U6);身份由 U1 DB marker 锚定 | 部分已核实,全表清单待核对 |
| 2 | 个人档库(同 schema) | `<DataDir>/agent-system.db` | SQLite(app.go:196 已核实) | 在线一致性备份接口(现有)+ WAL 同档复制禁止 | 已核实 |
| 3 | 工件对象(内容文件) | `<DataDir>/artifacts`(PG 档;app.go:179 已核实) | blob 临时写+rename(KTD11 对象化目标) | 文件+DB 元数据配对恢复,hash 对账 | 已核实路径,对象语义待 U6 |
| 4 | 上传/输入附件 | `<DataDir>` 下上传子目录 | 同上 | 同上 | 路径待核对 |
| 5 | 文档大内容/版本 | DB 行 +(如启用)内容文件 | documents 域 | DB 恢复+文件对账 | 待核对 |
| 6 | 模型转录(Native Session) | 各工作区 `<sandbox>/.agentx/transcript.jsonl` | llm_direct.go:245 起播种/重建(已核实) | 随工作区恢复;revision 不符可由消息库重播种(已核实的 reseed 机制) | 已核实 |
| 7 | 子代理派生请求 | 工作区内派生请求文件(writeSubagentRequest 已核实存在) | 落盘凭据 | 随工作区恢复 | 机制已核实,路径清单待核对 |
| 8 | 执行工作区/overlay/checkpoint | 受管 workspace 目录 + filestate | KTD7 账本对象 | 工作区对账+副作用记录 | 待核对 |
| 9 | 任务 Git 现场(worktree/未提交文件) | 各任务 worktree | git | 现场+基准 SHA 对账(U6/U7) | 待核对 |
| 10 | 插件自有存储 | skillsRoot 与 sidecar dir(host_v2.go:18/24 已核实机制);各启用插件自有 DB/目录 | 插件自定义 | 插件恢复适配器(r2 §3:必需插件不可恢复则阻塞全系统签收) | 机制已核实,逐插件清单待核对 |
| 11 | RDM 等下游模块存储 | 模块自有目录/库 | 模块自定义 | 同上,纳入兼容清单后覆盖"所有数据"承诺 | 待核对 |
| 12 | 配置与机密 | `runtime/<name>/svc.env`(迁移后 manifest.json+dsn_ref)、`.svc/<name>.json`、后端模型密钥(部署库) | 文件/表 | 受限配置备份(U6);机密独立可恢复 | svc.env 已核实;密钥存储待核对 |
| 13 | 派生状态(可重建,不作权威) | 检索/向量索引(collab U6 向量域)、Redis 缓存(如启用)、`runtime/<name>/history.jsonl` 事件史 | 索引/日志 | 从权威源重建(r2 §5.2 索引行);Redis 可清空 | 部分待核对 |
| 14 | 程序/资产组合 | bin 制品、Web 资产、插件二进制 | 不可变组合 | 组合 manifest+摘要(U1 §3 program 字段、U6 备份) | 已核实(deploy source registry) |
## 清单纪律
- 进入 U6 备份范围与 U7 签收的每一行,实施增量必须把该行"待核对"清零并附证据;无法证明可恢复的必需项按 r2 §3 阻塞全系统签收。
- 新增插件/下游模块上线时必须同步登记本清单(守护测试在 U7 范围评估)。
+82
View File
@@ -0,0 +1,82 @@
# U1 实例与数据身份 manifest 合同(设计 v0.1)
状态:方案设计候选,待人工评审后进入实施。主记录:服务重启与全量数据恢复方案 r2(`modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md` §7 U1 与 KTD1/KTD2;该提案落在本仓 feature/service-restart-recovery-plan 分支待推送合入,合入后即为上述路径);本任务卡 `feature/recovery-u1-instance-identity`。授权:用户批准 r2(2026-10-12)并指示启动 U1;实施(改 `scripts/svc.sh` 与测试)待本设计评审通过。
基线:Dev `89174a0`(含 svc-autorecover 合入:autostart 期望态、`recover`、watchdog、`.start.lock`)。事实来源 SELF·源码观察;本文所有"已核实"均可在该基线复核。
## 1. 目标与不变量
持久配置是启动权威,PID 与临时环境只是观察/覆盖。五条不变量对应 U1 验收:
- **I1 连续性**:连续裸启停(无参数)使用同一 DB 身份与数据/工件/工作区根。
- **I2 非漂移**:只改端口/监听/程序版本/调用 cwd,不改变 DB、数据根与插件组合。
- **I3 原子性**:配置提交是整份原子操作,任意 kill 窗口只可能读到完整旧版或完整新版。
- **I4 阻塞不重建**:必需配置缺失、DB 身份不匹配或数据根 marker 丢失时拒绝启动,绝不新建空库/空目录掩盖。
- **I5 机密引用化**:DSN/凭据不进 manifest 正文与日志,只存受保护文件引用。
## 2. 现状(基线事实)
- 每实例配置位于 `runtime/<name>/svc.env`(扁平 `key=value`,键含 kind/dbref/dsn/auth/exec/addr/autostart/config/source 等),`svc_get`/`svc_set` 逐键读写(已核实)。
- `resolve_db_config()`(svc.sh ≈1373)统一解析 dbref/DSN/config,裸启停复用绑定,绑定失效拒绝启动(已核实)。
- desired state `autostart` 已持久化于 svc.env 并被 recover/watchdog 消费(U2 首切片,PR #396/#401,已核实)。
- 生命周期互斥 `.start.lock` 覆盖 start/stop/restart/deploy/update/recover(已核实)。
- 缺口:逐键更新无原子性(kill 窗口可留中间态);参数重建路径可漂移自定义 data/plugins;无配置 revision 与 CAS;数据根/DB 无身份 marker("看起来是同一目录"≠同一身份);首次 init 与常规 start 无入口区分;`dsn=` 明文存 svc.env。
## 3. manifest v1 结构
文件:`runtime/<name>/manifest.json`(权威)。svc.env 迁移后由 manifest 派生生成、仅供未改造消费方只读(见 §6)。
| 字段组 | 字段 | 说明 |
|---|---|---|
| 元 | `schema`(=1)、`revision`(int,单调)、`updated_at` | 整份 CAS 的版本轴 |
| 身份 | `instance_id`(稳定,创建后不变)、`service_group`、`created_at` | R2 的"实例 ID" |
| 期望态 | `desired`: `running`\|`stopped` | 吸收 autostart;U2 watchdog/recover 改读此字段(映射:autostart=1⇔running) |
| 程序 | `program`: `{source, exec, digest}` | deploy source registry 的组合锚点 |
| 数据库 | `db`: `{mode, dbref, dsn_ref, auth}` | `dsn_ref` 指向 0600 密钥文件路径,正文无 DSN |
| 路径 | `paths`: `{data_root, artifacts_root, workspaces_root, plugins_dir}` | 绝对路径;I2 要求变更端口/程序不动这些 |
| 监听 | `listen`: `{addr, port}` | 显式覆盖白名单字段 |
| 配置 | `config`: `{file}` | 现 `.svc/<name>.json` 引用 |
| 依赖 | `deps`: `{required:[], optional:[]}` | U2 依赖图输入 |
| 恢复 | `recovery`: `{on_boot, watchdog}` | 策略位,U2/U7 消费 |
| 身份锚 | `identity`: `{db_marker:{expected_instance_id}, data_marker:{path}}` | §5 校验目标 |
| 机密 | `secrets`: `[{path, keys[], perms}]` | 引用清单,供 U6 备份范围 |
## 4. 写路径与并发
- 所有 manifest 写与 start/stop/restart/deploy/update/recover 在该实例的 `.start.lock` 内串行(复用 U2 已建锁,不新造锁)。
- 提交流程:整份读改 → 校验 → 写 `manifest.json.new` → fsync 文件 → `rename` 覆盖 → fsync 目录 →(派生 svc.env 同法)。revision 在 candidate 内 +1;rename 前复核在位 revision,不一致则丢弃 candidate 重读重试(防御性 CAS)。
- 禁止对 manifest 逐键写;迁移期兼容层 `svc_set` 语义改为"整份读-改-写"。
## 5. 解析顺序与启动校验
- 解析:CLI 显式覆盖(仅 `listen.*`、`program.source` 等白名单字段)> manifest > 环境变量(迁移后仅作显式 override 记录,不再隐式成为下次事实)。
- 启动校验(全部通过才 exec):
1. schema/revision 合法且 revision ≥ 在位值;
2. **DB marker**:PG 专用表 `agent_system_identity(instance_id, dbref, schema_version, created_at)`;SQLite 同表。首建仅发生在 `svc init`;常规启动要求 manifest.instance_id 与 marker 匹配,缺失/不匹配即阻塞(I4,对齐 resolve_db_config 现有"绑定失效拒绝启动")。
3. **数据根 marker**:`<data_root>/.agentx/data-marker.json`(instance_id + roots 清单摘要);不匹配/丢失阻塞。
4. `dsn_ref` 指向文件存在且权限 ≤0600。
- 显式入口:`svc init`(首次:创建 manifest+双 marker,要求目标为空);`svc restore`(灾难恢复:新目标装配 manifest+marker,走 U6 验证合同)。`start` 永不隐式 init。
## 6. 一次性迁移
lock → 读全部 svc.env 键 → 构造 manifest v1(rev=1) → §4 原子写 → 原 svc.env 更名 `svc.env.legacy`(只读留存)→ 派生新 svc.env。kill 窗口语义:只存在 legacy(未迁)或完整 v1+legacy(已迁),两者并存时以 manifest 为准并幂等补齐派生;无第三态。
## 7. 现状刻画回归(实施 I2 的前置测试计划)
`tests/test_svc_identity.py`,桩二进制+临时目录模式沿 `tests/test_svc_recover.py`:
| # | 场景 | 判据 |
|---|---|---|
| T1 | 裸 start→stop→start×2 | 同一 DB 标识与数据根(断言 svc.env/迁移后 manifest 不变、marker 不重建) |
| T2 | 只改端口再启停 | dbref/data/plugins 组合逐字节不变 |
| T3 | 单独 data 覆盖 | 按合同显式生效或整体拒绝,无半套路径 |
| T4 | 迁移窗口注入 kill(写 .new 后 rename 前) | 下次启动只读到完整旧或完整新 |
| T5 | 缺凭据/错 dbref/删 marker | 拒绝启动,退出码非 0,不产生新库/新 marker |
| T6 | 更换 exec 路径与 cwd 调用 svc | 身份字段不变(对齐 r2 U1 验收新增条目) |
真实 PG 专项(marker SQL、跨库身份)单列隔离用例,沿用 test_svc_recover 的桩 http/健康探测 + 真实 pg 实例环境标记;缺环境时 NOT_RUN。
## 8. 验收映射与未决
r2 U1 验收五条 ↔ 本文:连续裸启停(§5/I1)、只改端口(I2/§3 paths)、data 覆盖(§5 白名单+T3)、迁移 kill(§6/T4)、缺凭据错库丢 marker(§5.2-4/T5);外加程序/cwd(T6)。
未决(评审请裁定):(1) `instance_id` 生成规则建议 `svc-<group>-<随机12>` 创建即冻结;(2) dsn 明文迁 `dsn_ref` 的备份影响(U6 范围);(3) manifest 与 `.svc/<name>.json` 应用配置的边界——本设计仅引用不合并;(4) Redis 等 optional 依赖在 deps 中的表达粒度。