# ADR-0363:有界 Application Shutdown Receipt 与物理 Service Stop 证据 - 状态:Proposed(产品 receipt、Owner consumer、真实 manager 组合门与物理 recorder 协议已完成;固定实机报告待采集) - 日期:2026-08-11 - 关联:QL-RFC-0001 D-196、D-274、D-275、ADR-0204、ADR-0206、ADR-0362 ## 上下文 root service bridge 的 stop outcome 能证明 systemd/OpenRC 最终观察到 service inactive、原 receipted PID/start identity 已消失,但不能单独证明 Application 完成了 有序 drain。manager 超时后 SIGKILL、进程崩溃或强制退出都可能产生相同的“PID 不存在”终态。native application recorder 可以从 stdout 看到 `stopped`,真实 init-managed release 则不能依赖 journal/syslog 的持久化与轮转策略。 因此物理 Edge 的 graceful-stop Gate 需要一个由 Application 在资源释放成功后发布的 持久事实,再与 root bridge outcome 和实时进程终态交叉验证。 ## 决策 ### 1. Shutdown receipt 属于现有 Local Application 生产进程 `@qinglong/local-application` 仅在收到首个 SIGINT/SIGTERM 且 `application.stop()` 返回 `stopped` 后发布: ```text .stopped.json ``` schema 为 `qinglong/local-application-shutdown-receipt@v1`,最大 4 KiB,包含: - instance、Profile、signal 与固定 `stopResult=stopped`; - 原 startup receipt SHA-256; - 同一 boot ID、PID/start ticks、Node executable/version; - stop 完成时的 Linux boot age; - domain-separated SHA-256。 `timed_out`、非 Linux、startup receipt 未成功发布或 shutdown receipt 发布失败均不能 形成 graceful 事实。Linux 发布失败会让 Application CLI 失败关闭。 ### 2. Startup/shutdown 共享一个文件安全协议 同 package 的 `production-process/lifecycleReceiptFile.ts` 统一实现: - current real/effective UID 必须一致; - parent 必须为当前 UID 控制且 group/other 不可写; - deterministic stage 使用 `O_EXCL|O_NOFOLLOW`,只复用 owner `0600`、single-link、 有界旧 stage; - write→fsync→parent identity recheck→atomic rename→final inode/mode/link/size recheck; - directory fsync 在不支持的文件系统上保持 best effort。 每次启动和停止各只替换一个 current 小文件,不追加历史,不启动 timer、watcher、 listener 或清理 daemon。实现留在现有 `@qinglong/local-application` 的 `production-process/` 领域目录,不新增 package 或依赖。 ### 3. Owner cutover stop 必须验证三种独立事实 adopted service stop 只有同时满足下列条件才可推进 `target_stopped`: 1. root bridge outcome 绑定当前 stop intent 且 manager state 为 `stopped`; 2. shutdown receipt 以 SIGTERM、startup receipt digest、boot、PID/start、Node 和 `stoppedBootAgeMs >= activeBootAgeMs` 绑定上一代 active record; 3. exact receipted PID/start identity 已不存在。 缺失或漂移的 shutdown receipt 进入 `manual_required`,原因固定为 `application_shutdown_receipt_unproved`。service cutover record 新增 `shutdownReceiptDigest`,使 instance head 的 source record 对 graceful 事实形成摘要 链。PID 消失不再单独足以推进 lineage。 ### 4. 物理 stop recorder 复用同一双 authority 协议 非 package 命令 `evidence:physical-edge-direct-service-stop` 提供: - `prepare`:读取并复验同 boot 的 direct start session/report、实时 receipt/PID 和 active/enabled init 状态,以 Owner 发布 fresh stop intent,输出固定 root bridge command;可选 `--root-command-output` 在 Owner data path 内以 `0600`、no-replace 写出与 stdout 完全相同的 command handoff; - `resume`:以 Owner 消费 stopped outcome,验证 shutdown receipt、旧进程身份消失、 service inactive 但仍 enabled,发布 `physical_edge_direct_service_stop_candidate`。 direct start 的 `prepare` 同样提供 `--root-command-output`。运维流程要求 operator 先把该 Owner handoff 精确安装到 canonical root-owned `0700` 目录中的 root-owned `0600` 文件并复核摘要,再以 root 调用 `ql3-service-bridge run --command-file`;禁止 用手工 `systemctl`/`rc-service` 绕过 intent、barrier、outcome 与 Owner resume。start bridge 必须返回 `active`,stop bridge 必须返回 `stopped`,然后才能分别 resume。 session/report 绑定 direct session、active report、startup receipt、boot、PID/start、 stop intent/outcome/observation digest。统一 physical importer 只在 stop report 与同一 direct start report 精确配对时增加 `init_managed_graceful_application_stop`;否则该项继续留在 `remainingRequiredEvidence`。 ### 5. 证据边界 报告永久 `supported:false`。它不证明 disable/descriptor removal、突然断电恢复、 firmware shutdown、whole-device flash 写放大、release signature、Standalone 或 Cluster。graceful stop 也不能替代 power-loss Gate。 ## 验收证据 - 2026-08-11 的真实 arm64 Local image 门发现 Linux runtime observation 的字段插入顺序与 receipt verifier 的 canonical 顺序不同,导致同一语义对象产生不同 digest;builder 已改为逐字段 canonical materialization,并增加 Linux 属性顺序回归。shutdown/process 专项 15 pass/1 Linux skip;修复后的同一镜像在 Edge 128 MiB/64 PIDs 与 Standalone 256 MiB/256 PIDs、read-only root、network none 下均完成 active→graceful stop,SQLite integrity 为 `ok`。这修复的是既有 D-275 产品路径,不改变 receipt schema、authority 或固定物理设备剩余 Gate。 - Local Application startup/shutdown/process 专项:17 pass/2 条 Linux 条件 skip; - Owner service cutover/intent/contract:14/14,包含无 shutdown receipt 必须 `manual_required`; - 真实 systemd/OpenRC × UID 0/10001 fresh+adopted 组合门全部通过,四组 adopted active→SIGTERM shutdown receipt→target_stopped;报告 SHA-256 `28d5c29f91747f88b099c4918648199c21338fa48b9840c48d8027e70f14c954`,容器零残留; - direct stop recorder/report/importer 与既有 physical 回归 35 pass/1 Linux skip; - direct start/stop/aggregation handoff 与运维顺序专项 15 pass/1 Linux skip,回归固定 start prepare→root bridge→start resume→stop prepare→root bridge→stop resume; - service bridge 的 systemd/OpenRC × UID 0/10001 四容器组合门再次全绿,覆盖 root command file、exact replay、active/stopped observation、Owner outcome 与 adopted cutover;报告 SHA-256 `0120972416d679ef3749e81e99f65ead5ca45bd29f43e4a1e624d675d8f4fc9a`,容器、卷、 网络零残留; - 完整 backend 回归 1,155 pass/2 条条件 skip、0 fail;本机遗留 SQLite binding 使用 项目锁定的 node-gyp 12.4.0 构建并验证加载,未修改生产依赖声明; - workspace 仍为 17 package;Local Application 15 source 中 13 nested、2 个根 entry, 无 shallow/single-source package、无新增生产依赖或常驻资源。 固定物理 systemd/OpenRC 设备尚未执行 start prepare→root bridge→reboot→resume→stop prepare→root bridge→stop resume,因此本 ADR 与 D-275 保持 Proposed。 ## 不采用方案 - **只看 PID 消失**:无法区分 graceful drain、SIGKILL 与 crash。 - **只看 systemd/OpenRC stopped**:manager 不拥有 Application 内部资源释放事实。 - **解析 journal/syslog**:OpenRC/嵌入式日志持久化与轮转不统一。 - **新增 shutdown daemon/socket**:增加低配设备常驻内存、端口和 authority。 - **为两个 receipt 新建 package**:它们只有一个生产者并共享同一文件安全边界,拆包 只会增加发布碎片。 - **把 graceful stop 当作断电证明**:两者故障模型相反,不能互相替代。