Files
qinglong/docs/adr/ADR-0362-dual-authority-system-service-bridge.md
T

165 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0362systemd/OpenRC 双 Authority Service Bridge
- 状态:Proposed(产品 bridge、Owner consumer、adversarial、双 manager/双身份 adopted+graceful-stop 组合门与物理 recorder 协议升级已完成;实机报告待采集)
- 日期:2026-08-11
- 关联:QL-RFC-0001 D-06、D-17、D-64、D-85、D-274、ADR-0194、ADR-0205、ADR-03090315
## 上下文
QingLong 3.0 已能为 Edge/Standalone 生成 systemd/OpenRC descriptor,也有真实物理设备的 init-managed
启动证据;正式部署仍要求管理员手工 `install`、enable/start。Docker cutover controller 不能直接推广:
它以最终服务 UID 运行,command、deployment root、journal 和 outcome 都必须是同一 UID 的私有
`0600/0700` 文件;system unit 安装、systemd system manager 与 OpenRC 通常属于 root。让 Owner CLI
直接 sudo,或让 root 读取/接管 Owner 私有 journal,都会破坏当前 UID、文件身份和 crash replay 边界。
路由器还存在两类真实形态:root-only OpenWrt/嵌入式设备可让 service UID 与 controller UID 都为 0;普通
NAS/Linux 则必须让常驻 QingLong 以非 root UID 运行、只有安装桥短暂取得 root。两者不能由一个隐式
“是否 sudo 成功”分支裁决。
## 决策
### 1. Owner 与 root bridge 是两个显式 authority
Owner deployment authority 先在既有私有 deployment root 中发布 no-replace service intent。intent 精确绑定:
- Profile、instance、cutover/generation 或 fresh deployment identity
- service kind 与固定 service name
- service UID/GID、deployment root、application config
- prepared descriptor 的 canonical source、SHA-256、预期 root destination/mode
- action ID、动作(install-enable-start、start/restart 或 stop)和请求时间;
- 上一代 active/stop/receipt/lineage digest(适用时)。
管理员使用独立 root-owned `0600` command file 调用短生命周期 `ql3-service-bridge`。bridge 必须要求
real/effective UID/GID 都为 root,不接受 setuid mismatch、shell string、任意 unit path、任意 service name、
调用方自报 active/stopped 或可修改的 manager 参数。
### 2. root bridge 只拥有平台副作用,不拥有应用状态
bridge 只允许:
1. 验证 Owner intent、deployment/descriptor owner/mode/realpath/digest
2. 将 exact descriptor 安装为 root:root `0644`systemd)或 `0755`OpenRC),existing 必须内容完全一致;
3. 以逐参数 `spawn` 执行固定 manager 动作;systemd 只能 daemon-reload、enable、start/stop、showOpenRC
只能 rc-update add/show 与 rc-service start/stop/status
4. 读取 manager 的 exact service/fragment/PID/enabled 状态;
5. 写 root 私有 durable barrier/outcome,并发布一个绑定 intent、descriptor 和 manager observation digest 的
Owner outcome。
bridge 不打开 SQLite,不读取 Secret/Plugin/AI material,不判断 application ready,不推进 cutover instance head
不重启 legacy,也不执行数据 reconciliation。Owner 收到 outcome 后仍必须自己验证 startup receipt、PID/start
identity、application config、activation/commitment、数据 evidence 和当前 lineage,才可写 `target_active`
`target_stopped`
### 3. 两侧 journal 独立且 crash-safe
root bridge 在固定 root-owned `0700` controller root 中按 action ID 保存 no-replace barrier/outcome;目标已存在、
descriptor drift 或 action ID 跨 intent 复用时失败关闭。barrier 发布后发生未知 start/restart 结果,重放只 inspect
不再次执行 manager mutation;无法证明 exact active/stopped 时产出 `manual_required`。stop 可以在 mutation 前保持
幂等,但一旦 barrier 已存在,同样优先 inspect,避免把新一代进程误当旧 action 停止。
交给 Owner 的 outcome 只能发布到 intent 预先绑定、位于 Owner `0700` 目录内的新路径。bridge 以同目录随机 stage
写入、fsync、fchown 为 service UID/GID、hard-link no-replace、父目录 fsyncOwner 只接受自身拥有的 `0600`
regular file,并复验 bridge outcome digest。root journal 与 Owner outcome 任一缺失都不能靠另一个布尔值补齐。
### 4. root-only 与非 root 部署使用同一协议
root-only 设备必须显式 `allowRootService=true`service UID/GID 均为 0Owner intent、bridge command 和 outcome
仍分阶段发布,不能退化为一条 shell。非 root 部署使用 service UID/GIDbridge 仅在 outcome 最终发布时把文件
交还该 identity。两种模式共享 schema、状态机和审计器,避免形成“路由器快捷旁路”。
### 5. 代码与 package 边界
首版放入现有 `@qinglong/local-owner-cli``deployment/service-manager/` domain,并增加独立 binary entry;它与
现有 deployment/Docker cutover 共享安装制品、command-file 协议和 release cadence,不新增常驻依赖。root bridge
入口只能静态到达 command-file 与该 domain,不能 import Owner console、SQLite、AI、Docker cutover 或 application。
暂不新增第 18 个 workspace package。只有未来 root helper 需要独立 OS package、不同 Node/native 依赖、独立签名/
更新责任或必须从 Owner 安装闭包物理排除时,才以这些真实部署证据重新评审;“root 权限听起来重要”或文件数量
本身不足以拆包。
## 不采用方案
- **Owner CLI 直接执行 sudo/systemctl**:交互、policy、环境和 effective UID 不稳定,command-file owner 语义被破坏。
- **root 直接消费 Owner command/journal**:现有协议明确要求 current UID owner;放宽会让全部 Owner ceremony 扩权。
- **Application 自己管理 init service**:常驻进程将持有 root/service-manager authority,违反最小权限与自更新隔离。
- **只记录 manager exit code**:响应丢失、manager 接受但服务失败、PID/unit 被替换都无法裁决。
- **为 bridge 立即新增微型 package**:当前没有独立发布或依赖闭包收益;先用独立 entry 与 import gate表达权限。
- **只支持 root-only 路由器**:会把 NAS/普通 Linux 的非 root 常驻要求推回手工旁路。
## 验收门
1. contract 必须拒绝未知字段、任意 service name/destination、非 root bridge identity、symlink、错误 owner/mode、
descriptor drift、action replay drift 和 shell-shaped manager 参数。
2. systemd 与 OpenRC 都必须覆盖 fresh install/start、exact replay、start response loss、stop、descriptor drift、
replaced PID/unit、barrier 后 crash 和 Owner outcome no-replace。
3. Linux 容器门必须运行真实 systemd 与 OpenRC managerfake runner 只属于单元故障矩阵,不能代替最终 manager gate。
4. 物理 Edge recorder 必须通过同一产品 Owner intent/root bridge/Owner outcome 安装固定 `qinglong3` unit,再证明
boot→init→direct release active、graceful stop 与 root bridge 产物完全相同;容器门不替代
固件启动、断电、flash 或低内存证据。
5. package boundary 必须保持 17Edge/Standalone 常驻 artifact 不加载 bridgeCluster/Worker 不安装或导入该入口。
## 当前实现与证据
2026-08-11 已在现有 `@qinglong/local-owner-cli/deployment/service-manager` domain 完成首个产品闭环:
- Owner 的 `service-intent-prepare` 从当前私有 `local-application.json` 与 prepared descriptor 派生 Profile、
instance、UID/GID、canonical path、mode 和双方 SHA-256,以同目录 stage→fsync→hard-link no-replace 发布
`0600` intentadopted generation 1 只允许 install/start/stopgeneration 2+ 只允许 restart/stop。Owner 还会
读取当前 instance head 并执行 compare-and-swap 前置校验:首次 install/start 只能绑定
`legacy_stopped@generation 0`restart 只能绑定上一代 `target_active`stop 只能绑定当前代
`target_active`,且 `previousRecordDigest` 必须等于 head source record;已有 head 时禁止以 `fresh` 绕过。
- 独立 `ql3-service-bridge` binary 只接受 root-owned `0600` command,要求 real/effective UID/GID 全为 0
root 对显式 Owner `0600` intent 做 lstat→`O_NOFOLLOW`→fstat identity read,不放宽通用 current-UID
command-file reader。descriptor、application config、Owner outcome directory、manager executable 与 destination
parent 均按 owner/mode/realpath/digest 失败关闭。
- root controller 先发布 action barrier,再进行 exact descriptor install 和固定 argv manager mutationbarrier 后
replay 只 inspect。root outcome 先 durable no-replace,再通过 `fchown(service UID/GID)``0600`、hard-link
no-replace 与父目录 fsync 交给 Owner。Owner `service-outcome-consume` 重新验证 intent/outcome digest、当前
descriptor/config、UID/GID、fragment 与 manager observation,不把 exit code 当 active 事实。
- 独立 Owner `service-cutover-consume` 只接受 adopted `@v3` application binding,将 manager outcome 与当前
activation、legacy silence commitment、source/target/recovery/manifest 私有稳定身份汇合。active 还必须取得
与上一代不同的 startup receipt,并从 `/proc` 复核 service UID、manager PIDsystemd)、process start ticks 与
Node executablestop 必须证明原 receipted PID/start identity 已消失。consumer 先 no-replace 发布 digest 串联的
service cutover record,再 CAS 推进 instance head;崩溃重放从 record/head 收敛,不重复 manager mutation。
- systemd 只执行 daemon-reload/enable/start/restart/stop/showOpenRC 只执行 rc-update add/show 与 rc-service
start/stop/statusrestart 为显式 stop→start。fake runner 故障矩阵覆盖 systemd response-loss 后 PID 变化、
exact replay 不重复 mutation,以及 OpenRC 固定 argv/response-lossroot Linux 基础与 adversarial 定向门 5/5。
- 真实 Docker gate 使用 systemd PID 1 与 Alpine 的真实 rc-update/rc-service/openrc-run/supervise-daemon,在四个
隔离容器中分别覆盖 systemd/OpenRC × root UID 0/非 root UID 10001。四组都完成 root command、exact
descriptor install、enable/start、Owner outcome consume、exact replay、stop 与 `/proc` service UID 证明;
systemd 双身份和 OpenRC root 还完成 restart。四组随后全部切换到 adopted v3 fixture,由真实受管 Node 进程
原子发布 Linux startup receiptOwner consumer 对真实 `/proc`、activation/commitment/static data 执行复验并
分别推进 `target_active → target_stopped`。总 gate 新增 `adoptedCutoverActive/Stopped=true`,最新报告 SHA-256
`267f8f3cb4145db6fb8acd5a3690352a78694cd25e2f6c1c483f0ef2227c85a0`,结束后相关容器为零。该轮还发现并修正
gate 中缺失 `storage.mode=fresh` 的伪 v2 fixture;产品 intent parser 继续对该无效配置失败关闭。
- Local Owner service bridge/consumer 定向测试本机 13 pass/5 root-only skiproot Linux 5/5;完整 Owner 套件
唯一一次非通过是 sandbox 禁止绑定 `127.0.0.1`,该原有 Provider 测试在允许本机
loopback 后 9/9。package boundary 仍为 17dependency audit 零 findingEdge import compatible;独立
bridge import audit 强制禁止 SQLite、Owner Console、AI、Docker cutover 与 application 进入 root binary。
- Owner cutover consumer 新增 5/5:首次 active journal→head、相同 outcome/head 幂等重放、restart 复用旧 receipt
进入 `manual_required`、stop 仅在原 PID/start identity 消失后推进,以及 legacy source/recovery 内容漂移在 head
mutation 前失败关闭,manager PID/receipt PID 替换也进入 terminal manual。root adversarial 5/5 覆盖 systemd/
OpenRC response-loss inspect/replay、descriptor drift、barrier 后 crash+installed unit replacement、Owner outcome
no-replace;既有 Docker cutover 24/24 回归通过。
- 既有 `evidence:physical-edge-direct-service-start` 已移除旧的直接 root install/enable planprepare 现在以 Owner
发布 fresh `install-enable-start` intentsession 固定正式 `qinglong3` unit 并绑定 action/intent/outcome/root
controlleroperator 只得到固定 root bridge command。新 boot 的 resume 先以 Owner 复验 active outcome,再将
outcome/observation digest 与当前 boot receipt、init/PID、descriptor 和 artifact 一起写入 candidate report;统一
importer 会重新裁决 `owner_intent_root_service_bridge_owner_outcome`。该升级不新增 package、依赖或常驻资源。
- ADR-0363 增加 Application shutdown receipt 与 direct service stop recorder。Owner adopted stop 现在必须同时验证
root bridge stopped outcome、与上一代 startup receipt 精确绑定的 SIGTERM/stopped receipt,以及旧 PID/start
identity 消失;缺少 receipt 会进入 `manual_required`。真实四容器组合门已用 systemd/OpenRC × UID 0/10001
证明该链路,最新报告 SHA-256 为
`28d5c29f91747f88b099c4918648199c21338fa48b9840c48d8027e70f14c954`,容器零残留。
仍未完成且不得由上述证据替代:固定物理 Edge 上实际执行 root bridge/reboot/start-resume/stop-resume 得到的
start+graceful-stop 配对 report,以及断电、flash、低内存证据。因此本 ADR 与 D-274 继续保持 Proposed,管理员手工安装/启停仍是正式
运维路径。
## 影响
该决策明确关闭“直接把 Docker adapter 改名为 systemctl”的错误路线,并提供同时覆盖 root-only 路由器与非 root
Linux/NAS 的可恢复权限模型。当前状态仍为 Proposedbridge contract、产品 binary、Owner outcome/lineage
consumer、adversarial、双 manager/双身份 adopted 组合真实门和物理 recorder 协议升级已经完成,但物理设备报告尚未采集;运维文档中的管理员显式安装/启停步骤继续是唯一受支持路径,
D-64 不能标记完成。