Files
HomeAgent/deploy/hologramagent.service
JianFeeeee ba281a855a feat(update): 差分 OTA + 安装形态分派 + 事件总线统一受安全监控
三条主线,由 e2e 实测驱动。

## 一、差分 OTA(internal/update/delta.go)

实测:改一行逻辑,88MB 的二进制逐字节只差 80 字节,而 OTA 一直下整包。

用 rsync 族的滚动哈希块匹配(4KB 粒度),不是 bsdiff —— 后者需 17×N
峰值内存(88MB → 1.5GB),而升级恰可能发生在内存紧张时。

真实二进制实测:88,036,728 字节 → 补丁 77,316 字节(0.088%)。

三道闸:应用前验旧文件 sha256(错基线会静默产出垃圾)、应用后验结果
sha256、COPY/LITERAL 越界防护。任何失败**退回整包**,不向上抛 ——
差分是优化,不是可用性前提。

接入:Source.Resolve 优先试 delta;CheckRelease 按 -from-<commit8>.delta
匹配(不猜基线);CLI 新增 -make-delta;打包脚本 build_delta(需
PREVIOUS_HOMED),delta 进 SHA256SUMS。

## 二、安装形态分派(internal/update/install.go + channel.go)

内核对「能不能自己升级」此前只有一个答案:直接替换二进制。而 deb/rpm
形态下那样做会让 dpkg/rpm 数据库与实际内容不一致(下次 apt upgrade
覆盖回旧版,或 dpkg 拒绝后续升级)。

新增探测:deb/rpm/tar/container/source 五类形态 + 变体(full/server/
client/toolchain)+ 目录写权限。判据全部来自可观测系统事实(dpkg 的
info 目录、容器标记文件),不做路径推断。

三端统一走 PlanUpgrade 分派;形态闸门放在**下载 880MB 之前**。

## 三、事件总线统一受安全监控(internal/events + security)

用户指正:「eventbus 上所有事件都要接受安全模块的监控」。

此前 EventPolicy 只被 internal/sdk/events.go 调用,而那是四条路径中的
一条。s.Publish / s.Subscribe / s.EventBus() 拿的是裸 *events.Bus,
任何插件都能绕过策略发任意事件(含伪造内核事件)。

现在闸门在 Bus.Publish/Subscribe 本体上(Guard 接口注入,避免 events
↔ security 成环),四条路径全部覆盖。sdk.EventBus() 改返回带身份的
BusHandle。内核身份免授权,但载荷照样过凭据闸门。

白名单按主体收窄:agent_output/stage/terminal_output 只允许承载者
(webui/cli/agentcli)发布 —— 它们是内核向外的输出面,谁都能发就等于
能伪造 agent 说的话。

## e2e 暴露并修掉的三个真实回归

	[bus] subscribe denied: pluginmgr type=update_request
	[bus] subscribe denied: skillmgr type=skill_detected

前两个让升级请求静默丢失、职责链交接断掉。内部协作事件必须可订阅
(安全边界在发布侧,订阅只是「听得到」)。

## 顺带的止血修复

- 回滚判据从 `--version` 改为 digest:旧内核(1.3.x 及以前)没有该
  flag,而它恰是最常见的回滚目标 —— 原逻辑让「从旧版升级失败」永久
  无法回滚。
- CopyFile 补 fsync(替换 homed 本体时掉电会留半截文件)。
- restore 完整还原才清 marker(原先先清再还原,还原失败就失去第二次
  自愈机会)。
- 回滚时移除升级中新增的 depgraph(否则「旧 homed + 新 depgraph」
  会因 digest 不匹配崩溃循环)。
- homed-upgrade 不再硬编码 systemctl restart(丢 guard 状态、unit 名
  漂移);改用内核已有的 restartCh → exit 42 语义。
- homed-upgrade 不再用 CGO_ENABLED=0 编却依赖 go-sqlite3(stub 下
  静默失效,该路径从未工作过);改用 internal/config。
- 服务单元补 User=hologramagent + 三条安装路径补建用户(此前发行物里
  完全没有落实降权)。
- rpm 的 fpm 调用补 --after-install(此前 rpm 装完根本不建用户)。

## 判据

新增 30 条:差分 8(往返/压缩比/错基线/截断/篡改/边界)、通道 8、
执行器 6、事件闸门 8、按主体授权 8。全仓 go test ./... 0 失败。
2026-10-29 09:54:55 +08:00

133 lines
5.9 KiB
Desktop File
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.

[Unit]
Description=HologramAgent - 24/7 AI Butler
Documentation=https://github.com/trueagent/HologramAgent
After=network-online.target docker.service
Wants=network-online.target
# ★★ 为什么默认就走 guard(父守护)模式(2026-10-17 对齐本机生产实例)
#
# guard 是**双进程**架构:
# homed --role=guard 只负责拉起/看护 worker,自己不初始化内核
# homed --role=agent worker,跑真正的新版模块化启动(依赖图定序 + BFS 分层)
#
# 它带来的能力是单进程模式**没有**的:
# · 崩溃自愈:worker 崩了 guard 用**当前二进制**重新拉起
# · 安全模式:连续崩溃超 max_restarts → 自动进 failback 受限启动
# (只加载 webui/pluginmgr/recoverydiag,跑恢复梯子)
# · 卡死检测:IPC PING + 心跳 mtime,带启动宽限
# · 热更新:worker 以 exit 42 优雅退出 → guard 用新二进制重拉,
# **不经过 systemd**、不计崩遣次数、guard 进程保持存活
#
# 为什么不是 drop-in:单文件发行时 drop-in 容易被漏装。写在主单元里,
# 装完即是生产同款行为。要回退成单进程,只需把 ExecStart 的 --role=guard 去掉。
[Service]
# Type=exec(不是 simple):simple 会在 fork 后**立刻**认为启动完成,
# 于是 ExecStartPost 与 LoadCredential 的建立可能竞争。exec 会等到
# exec() 真正成功才继续,凭据目录与 ExecStartPost 的顺序才是确定的。
Type=exec
# ★ 降权运行(2026-10-28 落定)。
#
# 此前本单元**没有** User= ⇒ 以 root 运行。生产实例之所以是
# hologramagent,完全靠现场手工 drop-in —— 也就是说发行物里并没有
# 落实降权,装到新机器上就是 root。
#
# 配套要求:用户与组由包脚本(deb postinst / rpm %pre / install.sh)创建。
# 缺失时 systemd 会因 User= 不存在而拒绕启动 —— 这是**响亮失败**,
# 比默默用 root 好。
#
# ⚠️ 降权后 `Apply` 无法替换 /usr/bin/homed(属主 root、目录不可写)。
# 这不是缺陷,是设计:是否允许自替换由 update.Capabilities 判定,
# deb/rpm 形态一律走包管理器(见 internal/update/install.go)。
User=hologramagent
Group=hologramagent
# ★ guard 模式(与本机生产实例一致)。
# 数据目录默认 /var/lib/hologram(Debian 惯例);迁移到别的目录时
# 必须**同时**改这里和 core.daemon.data_dir —— 两者不等价,见 README。
ExecStart=/usr/bin/homed --role=guard --data /var/lib/hologram
Restart=always
RestartSec=10
# ★ 指数退避(对齐本机 zz-restart-backoff.conf)
#
# 为什么需要:RestartSec=10 是**固定**间隔,而 StartLimitBurst/Interval
# 在这些长驻 agent 上是故意放松的(收紧会让故障单元进入终态 failed、
# 必须人工 reset-failed —— 与"自愈"相反)。结果就是一个永久坏掉的服务
# 以固定频率无限重试,systemd 一句都不说。
# 实测(2026-09-05,llmsproxy):每 5 秒拉一次,连拉 79 分钟 = 836 次重启。
#
# RestartSteps 让间隔从 RestartSec 起指数递增到 RestartMaxDelaySec:
# 崩溃循环会衰减到每 5 分钟一次,服务不会被放弃(病根修好后自己回来),
# 日志也不再被刷爆。
RestartSteps=6
RestartMaxDelaySec=5min
# 关机上限 25s(对齐本机 zz-shutdown-timeout.conf)。
# 实测本服务正常退出 <1s;给 guard 转发 SIGTERM → 等 worker → Kill 留足余量,
# 而默认 90s 在本机从未用到,属于纯粹的风险敞口。
TimeoutStopSec=25s
# KillMode=control-group(默认):guard 与 worker 同 cgroup,
# 一起收,双保险不留孤儿进程。
KillMode=control-group
WorkingDirectory=/var/lib/hologram
StateDirectory=hologram
Environment=HOME=/root
# onnx 运行库随包安装(不在 /opt,见 packaging/package-linux.sh)
Environment=ONNXRUNTIME_DIR=/usr/lib/hologram/onnxruntime
# ★ 主密钥生产注入(security/key.go)
#
# 优先级:LoadCredential > HOMED_SECURITY_KEY(env) > <datadir>/security.key
#
# 为何用 LoadCredential:环境变量对同用户进程**可见**
# (/proc/<pid>/environ),任何以该用户身份运行的进程都能读到主密钥。
# LoadCredential 把内容放到 tmpfs($CREDENTIALS_DIRECTORY,仅本进程 0400),
# 不在磁盘留副本、也不进 environ。
#
# 生成密钥(一次性,然后妥善保管):
# head -c 32 /dev/urandom | xxd -p -c 64 | sudo tee /etc/hologram/security.key
# sudo chmod 600 /etc/hologram/security.key
#
# ⚠ 该文件就是数据的主密钥:**丢了它,已加密的变量全部解不开**(响亮失败,
# 不会静默重建)。请与备份一并保管。
#
# ⚠ 未配置时**不要**留一个空文件 —— 凭据目录一旦被设置而凭据缺失,内核会
# **拒绝启动**(不静默回退到磁盘密钥)。若要用磁盘密钥路径,删掉下面这行。
#
# ⚠ 从旧版(/var/lib/homeagent/security.key,改名前的路径)升级时:
# 内核支持"磁盘钥 + 内嵌钥"并存解密(AdditionalDecryptKeys),
# 所以先放 /etc/hologram/security.key 与旧磁盘钥**同一份内容**再启用本行,
# 否则旧密文解不开。
LoadCredential=homed_security_key:/etc/hologram/security.key
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=false
DevicePolicy=closed
# 只允许读主密钥所在目录,其余系统路径保持只读
ReadOnlyPaths=/etc/hologram
# 插件需要写自己目录 + 数据目录 + 插件沙箱
ReadWritePaths=/var/lib/hologram
# Audio devices
DeviceAllow=/dev/snd rw
DeviceAllow=/dev/dsp rw
# Resource limits
LimitNOFILE=65536
LimitNPROC=256
# Chinese-CLIP 本身实测约 1.15GB;加文档稠密索引、词向量、插件与
# ORT arena 后生产实例约 4.5GB。2GB 会在首次全量建索引时被 cgroup OOM。
MemoryMax=8G
CPUQuota=100%
[Install]
WantedBy=multi-user.target