Commit Graph
3 Commits
Author SHA1 Message Date
chick 9949cfe5b0 fix(install): 刚装完的服务会失败,单元要说清楚而不是每 5 秒重启一次
我在真内核上把安装序列完整走了一遍(容器里补了 useradd/systemctl 的替身,其余全是真的),
暴露出一个只有真跑才看得见的缺陷:

装完 → `install` 启动服务 → 但那时**还没有配对的手机**,而守护进程**故意**拒绝空的设备列表
(一个什么都不能授权的 agent 不该坐在那里显得健康)。于是 `Restart=always` + `RestartSec=5`
变成每 5 秒重启一次,永远,journal 里刷同一行。实测:exit 1,"devices is empty, so no phone
could ever be authorised"。

配对发生在安装**之后**(运维在节点上跑 `--pair`,然后在手机上配对),所以这不是异常状态,
而是流程的必经一步。修法两条:

- 单元加 `StartLimitIntervalSec=300` / `StartLimitBurst=10`:十次之后 systemd 放弃,单元进入
  `failed`——一个运维看得见、能处理的状态,而不是无限重启。
- `install` 的结束语改成说清这一步:服务会退出并停住,下一步是 `--pair`,用
  `systemctl status` 看状态。此前它说"installed and running",而那是假的。

同一轮演练确认了此前所有修复在真内核上成立:服务用户建了、配置与日志目录交给它了、配置对
该用户可读、单元来自二进制且内容一致、卸载按序拆干净(用户/目录/单元全无残留)。

Go 全包通过(新增一项守着"单元必须有启动上限,且 install 必须说出下一步")。
2026-09-18 01:09:15 +08:00
chick 3473816167 feat: 独立成仓库,自己拥有契约文件
agent 从 simadmin-relay 拆出来,作为公开仓库:安装命令要由节点执行,而节点没有凭据,所以
二进制必须放在匿名可读的地方,而 relay 源码与 IPA 继续私有。

拆出来之后有一处必须改,不是搬文件:`contracts/agent-policy.json` 原先由 relay 仓库拥有、
agent 侧是 vendored 副本(Makefile 从 `../contracts/` 拷、policy_test.go 断言两份相等)。
现在反过来——**agent 拥有策略**,因为 agent 是执行它的那一方,让它去拷贝一份住在别处的文件
本来就是别扭的。relay 保留 vendored 副本(它的限额从同一组数字推导),而两侧的一致性不再
靠构建期比较(跨仓库的构建期比较谁也做不了),改由运行时保证:agent 在握手时上报策略摘要,
relay 拿自己那份比对,在 `/v1/status` 里逐节点给出 `policyMatches`。这比原来的测试更强,
因为它比的是**实际部署的那两份**,而不是两个 checkout 里的文件。

`contracts/test-vectors.json` 同理:Go 实现生成它(`make vectors`),relay 的 Node 参考实现
与 app 的 Swift 测试从 vendored 副本重算。实测重新生成的结果与 relay 那份逐字节相同。

systemd 单元的 `Documentation=` 指向本仓库。`VERSION` 提到 1.0.0。

20 个提交的历史完整保留(filter-branch 按子目录抽取)。
2026-09-17 23:24:57 +08:00
chick 934760f8e2 fix(agent): 补上安装的服务那一半——建用户、改所有权、装单元
上一轮那段"一键安装"是坏的,而且只在节点上坏。实测三段里漏了三件:

1. systemd 单元写着 `User=msa-agent`,而命令从没创建这个用户;
2. `enroll` 是 sudo 跑的,配置是 root 所有 0600,服务以另一个用户运行——读不到自己的私钥;
3. 单元从 `msa-agent.service` 这个仓库里的路径拷,而节点上没有仓库。

三段都"看起来对",因为它们各自是一行合理的 shell;错的是它们与单元之间的接缝。这正
是这个项目里反复出现的那一类缺陷。

现在有 `msa-agent install`:

- 建 `--system --no-create-home --shell /usr/sbin/nologin` 的用户(已存在则跳过);
- 建日志目录,并把配置、配置目录、日志目录 chown 给该用户;
- 把 systemd 单元写进去。单元**嵌在二进制里**(`go:embed`),不是从仓库路径拷的,也不是
  从 URL 下的——一个能发 unit 的服务器就能给节点塞一个后门 unit,那与"中继不可信"是同一
  条理由。`--print-unit` 可以在装之前把它打出来看;
- `daemon-reload` 并 enable/start。

幂等:每步先看已经是什么状态,所以升级或上次失败后重跑都安全。`--dry-run` 打印计划且什么
都不改,并且在缺二进制或配置时**不拒绝**——"还没装"正是会去跑 dry-run 的状态,它只把缺的
东西指出来。chown 走 Runner 而不是 os.Chown,这样整条序列都能在无 root 的情况下测。

install 与 uninstall 共用同一份 `Paths`:装了什么没卸掉会在机器上留一份凭据,卸了什么没装
过会在新节点上失败。单元也一样,`TestVendoredUnitMatchesSystemd` 守着嵌入副本与
`systemd/msa-agent.service` 不漂移。

手机上的安装命令相应简化:`install` 那一步取代了原先手工的 useradd/install/systemctl 三行,
卸载那段也先给 `--dry-run`。

测试新增五项:单元不漂移、install 满足单元的全部要求(用户、所有权、单元来自二进制而非仓库
路径)、install dry run 什么都不改、缺二进制或配置时拒绝且一条命令都不跑、dry run 在全新
节点上不拒绝而是指出缺什么。变异验证:去掉 chown、去掉建用户、让 dry run 执行命令——三处都
让对应测试失败。

Go 全包、Node 134 项、Swift 116 项通过。
2026-09-17 17:55:57 +08:00