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

14 KiB
Raw Blame History

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 0644systemd)或 0755OpenRC),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_activetarget_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=trueservice UID/GID 均为 0Owner intent、bridge command 和 outcome 仍分阶段发布,不能退化为一条 shell。非 root 部署使用 service UID/GIDbridge 仅在 outcome 最终发布时把文件 交还该 identity。两种模式共享 schema、状态机和审计器,避免形成“路由器快捷旁路”。

5. 代码与 package 边界

首版放入现有 @qinglong/local-owner-clideployment/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 0restart 只能绑定上一代 target_activestop 只能绑定当前代 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 不能标记完成。