From b829de223b7a516f6b3206ed5eb0a3ced43df3d5 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 09:26:56 +0000 Subject: [PATCH] agent-exec transport 060f4b8e7beeb98874301408e53d3249/2c17fb638bb6 --- PROJECT.md | 3 +- docs/recovery/u8-sqlite-instance-guard.md | 65 +++++++++++++++++++++++ docs/svc.md | 26 +++++++++ modules/backend | 2 +- policy/architecture-baseline.json | 2 + 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 docs/recovery/u8-sqlite-instance-guard.md diff --git a/PROJECT.md b/PROJECT.md index 5a6c64a6..5d7dca38 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -54,12 +54,13 @@ U2 起实施与部署分别评审。 svc 启停、异常进程/主机退出后的依赖、配置/数据身份、任务/HITL/副作用、文件与组合备份恢复; r2 已批准。r3.1 增补 D1 实例身份收尾、D2 可回滚切换、D3 真实组合门禁,并记录 2026-10-12 rdmcand 数据归属:会话不在 dev 库的 rdmcand 窗口内,领域事件为空,不跨实例搬迁。 D1 与 D2 已合入 Backend #184、RDM #291、Dev #485。D3 的 `make test-lifecycle` 跑真实组合用例;登录仍是 NOT_RUN,依赖账户方案 U7/U5,不在本轮公共化账户 CLI。r3 其余未批条款仍待人工评审。 + 2026-10-12 用户批准 r3 新增(R11/R12、KTD13–15、U8):SQLite 活动服务锁与耐久核验已实施,故障注入、部署与运行验收仍 NOT_RUN。 - [svc 统一部署与一次性登录 Key 方案 r1](modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md): 二进制/源码统一部署、环境准备、无初始密码开户与账户恢复;一次性 Key 仅兑换设密受限会话; 2026-10-12 已批准;U1/U4 实施启动中;规格见 `docs/svc-deploy/`。 -上述账户/恢复方案已有 U1/U4 与 recover/watchdog 基础源码。2026-10-12 的 dev 8788 续查形成账户 r2、恢复 r3 **候选草案**:帮助与能力发现、账户事务/审计/登录接线、SQLite 活动服务隔离及备份恢复增补。新增内容待人工评审;Documents 独立分支 `feature/svc-account-durability-plan` 仅本地保存,不推进本仓 gitlink;本入口原链接仍指固定版。候选交接与核查见[续审记录](docs/svc-deploy/2026-10-12-account-durability-plan-review.md)。 +上述账户/恢复方案已有 U1/U4 与 recover/watchdog 基础源码。2026-10-12 的 dev 8788 续查形成账户 r2、恢复 r3 **候选草案**:帮助与能力发现、账户事务/审计/登录接线、SQLite 活动服务隔离及备份恢复增补。新增内容待人工评审;Documents 独立分支 `feature/svc-account-durability-plan` 仅本地保存,不推进本仓 gitlink;本入口原链接仍指固定版。候选交接与核查见[续审记录](docs/svc-deploy/2026-10-12-account-durability-plan-review.md)。U8 规格见 `docs/recovery/u8-sqlite-instance-guard.md`。 ## 验证 diff --git a/docs/recovery/u8-sqlite-instance-guard.md b/docs/recovery/u8-sqlite-instance-guard.md new file mode 100644 index 00000000..fafa3a4d --- /dev/null +++ b/docs/recovery/u8-sqlite-instance-guard.md @@ -0,0 +1,65 @@ +# U8 SQLite 活动服务锁与耐久核验 + +状态:2026-10-12 用户批准恢复方案 r3 的 U8 后编写,随 `feature/u8-sqlite-instance-guard` 实施。 +主记录:[服务重启与全量数据恢复方案](../../modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md) §7 U8、KTD13、KTD14。 +本文件只定本增量的可判定行为。账户 CLI 不迁移、不建库的合同由账户方案 U7 实现。 + +## 1. 锁对象 + +活动服务在打开 SQLite(迁移发生在打开内部)之前取得锁,并持有到存储关闭之后。PostgreSQL 档不使用这把锁,沿用迁移咨询锁与执行所有权。 + +| 规则 | 行为 | +|---|---| +| 路径 | 先 `Abs`,已存在的路径再 `EvalSymlinks`。数据库文件若是符号链接,锁放在解析后的真实文件旁边 | +| 锁文件 | `.lock`。`O_RDWR\|O_CREAT\|O_CLOEXEC\|O_NOFOLLOW`,模式 0600。永不 unlink。数据目录不存在时,活动服务在取锁前创建该目录 | +| 互斥 | `flock(LOCK_EX\|LOCK_NB)`。占用返回「活动服务已在运行」,调用方不得打开库 | +| 替换检测 | 取锁后比较 fd 与路径的 device/inode,不一致则关闭并重试,仍不一致则拒绝 | +| 硬链接 | 解析后的数据库 `nlink>1` 直接拒绝,不能用 adopt 绕过 | +| 平台 | `unix` 实现。其他平台的 SQLite 档拒绝启动。PostgreSQL 档不受此限制 | +| 范围 | 只挡活动服务与 `migrate`。`user`、`apikey`、`identity` 不取这把锁 | + +svc 的 `runtime//.start.lock` 只串行化脚本生命周期,挡不住直接执行二进制。本锁补的是这条缺口。 + +## 2. 身份记录 + +两处记录,各管一种替换: + +1. 锁文件内容(持有 fd 时读写):`v1 db_dev db_ino instance_id lock_gen`。`lock_gen` 是 16 字节随机值的十六进制。数据库 inode 与记录不一致,或记录还在而数据库文件消失,视为数据库被替换,**打开之前**拒绝。 +2. 表 `instance_guard`(迁移 v36,单行):记录锁文件的 device/inode、instance_id 与 `lock_gen`。守卫建立之后,锁文件 inode 与表不一致,或表里已有 `lock_gen` 而锁文件记录缺失/不一致,视为锁根丢失或锁文件被重建(含 inode 被回收后内容丢失),打开并迁移之后拒绝,且不改写表。 + +尚无锁文件记录、表中也无行:视为首次建立(含旧库升级),写入两边。这是升级路径,不是 adopt。 + +同时换掉数据库文件和锁文件、新库里也没有本表:无法与真正的首次建立区分。此时若设置了 `AGENT_SYSTEM_INSTANCE_ID`,仍由既有身份校验拒绝错库。该剩余风险写入任务卡,不在本增量用第二套锁文件解决。 + +`instance_id` 取 `AGENT_SYSTEM_INSTANCE_ID`。表和锁文件都已有非空值且与本次不一致时拒绝。本次为空则不比较,保留表内原值。 + +显式 `--adopt-instance-guard`(仅 `serve` 与 `migrate`)在已持锁的前提下重写两边记录。它只接受「数据库文件还在但 inode 已变」和「锁记录损坏」。记录还在而数据库文件消失时,adopt 也不打开、不新建空库。锁文件若是指向数据库的符号链接,或与数据库同一 inode,直接拒绝且不写入。硬链接拒绝不受该标志影响。 + +身份校验在写入守卫之前完成。`AGENT_SYSTEM_INSTANCE_ID` 已设置但与库内身份不符时,不留下本次的 instance id。 + +## 3. 耐久 + +SQLite DSN 增加 `synchronous(FULL)`。打开后在**同一应用连接**读回 `journal_mode=wal` 且 `synchronous=2`,否则关闭并拒绝。 + +PostgreSQL 从应用连接读 `pg_settings`:`fsync`、`full_page_writes`、`synchronous_commit` 任一为 `off`,或读不到,则拒绝。`synchronous_commit=local` 不是 `off`,不拒绝。 + +写失败必须把错误返回给调用方。测试用 `RLIMIT_FSIZE` 制造失败,并忽略 `SIGXFSZ`,以免进程被信号杀死而看起来像成功。 + +## 4. migrate 子命令 + +`agent-system migrate --data `:SQLite 先取活动服务锁,有服务占用则拒绝,再打开(打开即迁移)并走第 2 节的建立/拒绝规则。 + +`agent-system migrate --dsn ` 或环境变量 `AGENT_SYSTEM_DSN`:不取文件锁,迁移仍用 PostgreSQL 咨询锁排队。 + +## 5. 验收 + +| 编号 | 判据 | +|---|---| +| T8-1 | 两个进程启动同一 SQLite,后者在打开前失败;前者的 running 与 waiting_hitl 行保持原状态 | +| T8-2 | 符号链接与相对路径进入同一把锁;硬链接拒绝;数据库替换拒绝;锁文件删除后重建拒绝 | +| T8-3 | 持锁进程被 SIGKILL 后,同数据目录可以再次取锁并打开 | +| T8-4 | 本增量:服务持锁期间另一进程的短事务可提交;缺库的 OpenExisting 不建目录;migrate 与持锁服务互斥。账户 CLI 不迁移归 U7 | +| T8-5 | 换端口、工作目录或路径拼写仍落在同一锁文件上 | +| T8-6 | pragma 读自应用连接;超限写入返回错误 | + +`tests/test_svc_storage_ownership.py` 依赖 svc 启动路径。本任务不改 `scripts/svc.sh`,该文件留到账户 U5;若届时 svc 入口已稳定,再补脚本级用例。 diff --git a/docs/svc.md b/docs/svc.md index 94628f5a..ac70b6f2 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -907,9 +907,35 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 ## 13. 故障排查 +SQLite 档的活动服务锁在打开数据库之前取得,锁文件是解析后的 +`agent-system.db.lock`(不删除)。同一数据文件只允许一个 `serve` 或 `migrate`。 +`svc.sh` 的 `.start.lock` 只串行化脚本,挡不住直接启动二进制。账户短事务命令不拿这把锁。 +显式迁移: + +```bash +agent-system migrate --data <数据目录> +agent-system migrate --dsn "$AGENT_SYSTEM_DSN" # PostgreSQL,不取文件锁 +``` + +数据库被替换,或守卫建立后锁文件被删掉重建,启动会拒绝。确认是同一次实例后: + +```bash +agent-system serve --data <数据目录> --adopt-instance-guard +agent-system migrate --data <数据目录> --adopt-instance-guard +``` + +硬链接别名不能用 adopt 绕过。PostgreSQL 档在应用连接上读到 `fsync`、 +`full_page_writes` 或 `synchronous_commit` 为 off 时拒绝启动。 + | 症状 | 处置 | |---|---| | `端口被占用(非本服务)` | 按提示的占用者处理,或换端口(`--port` / `set --port --apply`) | +| `instance guard: sqlite service already running` | 已有进程占用同一 SQLite 文件:停掉那个服务,或换独立数据目录。`migrate` 同样会被拒绝 | +| `instance guard: sqlite database replaced` | 库文件 inode 与锁记录不符:确认不是拿错目录后,用 `--adopt-instance-guard` | +| `instance guard: recorded sqlite database is missing` | 锁记录指向的库不在了:恢复该文件,或在确认后 adopt | +| `instance guard: lock file was rebuilt` | 守卫已建立后锁文件被重建:确认后 `--adopt-instance-guard` | +| `instance guard: sqlite database has a hardlink alias` | 去掉硬链接别名后再启动;adopt 不能放行 | +| `durability:` | SQLite 未保持 WAL + synchronous FULL,或 PostgreSQL 的 fsync / full_page_writes / synchronous_commit 为 off | | `进程启动后即退出` | `scripts/svc.sh logs 200` / postgres 看 `runtime//pg.log`;history 里有 fail 记录 | | `20s 健康检查未通过` | 进程活着但未响应:查日志与端口;agent 常见原因是 DSN 指向未启动的库 | | `该产物不支持部分启动参数` | 提示而非错误:旧版本制品自动省略了不支持的 flag;需要该能力请部署新提交 | diff --git a/modules/backend b/modules/backend index b00e1924..7b379d28 160000 --- a/modules/backend +++ b/modules/backend @@ -1 +1 @@ -Subproject commit b00e1924407989e52350ecdfa17f16640815ed89 +Subproject commit 7b379d28e74fc695dcda4750ad64f223d41ec5af diff --git a/policy/architecture-baseline.json b/policy/architecture-baseline.json index af2a0b47..ab02da0d 100644 --- a/policy/architecture-baseline.json +++ b/policy/architecture-baseline.json @@ -8,6 +8,7 @@ "backend/app backend/genui", "backend/app backend/hostapi", "backend/app backend/ingest", +"backend/app backend/instanceguard", "backend/app backend/intentgate", "backend/app backend/intentsem", "backend/app backend/pgbus", @@ -31,6 +32,7 @@ "backend/ctxasm backend/vision", "backend/hostapi backend/store", "backend/ingest backend/vision", +"backend/instanceguard backend/store", "backend/pgbus backend/bus", "backend/pgbus backend/store", "backend/plugins backend/settings",