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

msa-agent

Runs on a SimAdmin node and lets a paired iPhone reach it through a public relay, without a VPN and without any inbound path into the internal network.

It opens no listening socket in normal operation. Every network action it takes is either an outbound HTTPS long poll to the relay or a loopback request to the SimAdmin on the same machine, which is what makes "no port mapping, no VPN" true rather than merely advertised.

iPhone ──HTTPS(signed, encrypted envelope)──> relay <──outbound long poll── msa-agent ──HTTP──> 127.0.0.1:<port>

The relay is a dumb pipe: it cannot read a request, cannot forge an answer and cannot send a command. The phone signs each request with its own private key and encrypts it to this agent's public key; this agent verifies the signature before touching the payload and re-checks the policy before dialing anything.

Install on a node

The agent is a static binary with no runtime dependencies and no third-party modules. enroll mints the node's keys and writes its config; install puts the systemd service around it.

sudo mkdir -p /opt/msa-agent /etc/msa-agent
sudo curl -fsSL https://gitea.chickliu.fun/Hermes/msa-agent/releases/latest/download/msa-agent-linux-arm64 \
  -o /opt/msa-agent/msa-agent
sudo chmod 755 /opt/msa-agent/msa-agent
sudo /opt/msa-agent/msa-agent enroll --relay https://your-relay --port 3000 \
  --code <one-time code> --write /etc/msa-agent/config.json
sudo /opt/msa-agent/msa-agent install

armv7 is the same build for 32-bit ARM. SHA256SUMS covers both.

With --code the node registers itself with the relay and does not need an entry written into the relay's agents.json by hand. Without it, enroll prints the entry to add.

Removing it again is sudo msa-agent uninstall; --dry-run shows the plan first, and --keep-logs keeps the audit trail.

Subcommands

Command What it does
(none) The daemon: handshake once, then poll forever
enroll Mints keys and identifiers, writes the config, optionally registers with the relay
install Creates the service user, gives it the config and logs, writes the unit, starts the service
uninstall Tells the relay first, then removes the service, unit, config, logs and user
version Prints the version stamped into the binary
--pair Opens the temporary LAN window a phone uses to pair; closes itself
--check Validates the config and policy, then exits
--once Runs a single poll cycle and exits

install --print-unit prints the systemd unit before installing it. The unit is embedded in the binary rather than fetched, so a node cannot end up running a unit from a different release than its agent.

Layout

Path Contents
contracts/agent-policy.json The authorisation surface: read/write allowlists, loopback enforcement, limits. Embedded in the binary; this repository owns it
contracts/test-vectors.json Deterministic vectors shared with the relay (Node) and the app (Swift), generated by cmd/msa-vectors
internal/wire/ The envelope: what is signed, what is encrypted, and in what order
internal/policy/ Loads and enforces the policy
internal/runtime/ Turns a verified envelope into one loopback call, or refuses it
internal/egress/ The only place a socket is opened toward SimAdmin
internal/agent/ The control loop: verify, decrypt, decide, execute, answer
internal/pair/ The temporary pairing window
internal/enroll/ Key and identifier generation, and self-registration with the relay
internal/install/ The service lifecycle: user, ownership, unit, and its reverse
internal/transport/ The outbound long-poll client
systemd/ The unit file, vendored into the binary by make vendor

Tests

make test          # go test ./... (vendors first)
make vet
go test ./internal/agent -fuzz FuzzHandleDoesNotPanicOnArbitraryEnvelopes

The agent is the one component whose input comes from a party the design assumes is hostile — the relay — so Handle is fuzzed as well as table-tested. A panic there is not a bad request: the agent is a single process holding one poll, so an unrecovered panic ends it and the node goes offline until systemd restarts it.

Three properties are worth stating because they are what the tests are about, and each one was a real defect at some point:

  • Ordering. Nothing that depends on the payload happens before the signature verifies, and nothing that touches SimAdmin happens before the policy says yes. Every refusal is classified, because a node with a drifted clock and a node under attack produce the same outward symptom.
  • The limiters are sliding windows. "At most N dangerous writes in any 60 seconds" is the property; a fixed window admits 2N across its boundary, and a token bucket admits a burst after any quiet period. The relay's per-device budget is the same algorithm, and the two are kept in step because they bound the same operation from two sides.
  • A request the phone has given up on is not executed. The relay answers 504 with "it may or may not have executed it", and that is only honest while it stays true. A batch is processed serially, so the last envelope of a full mailbox can begin well after its deadline; executing it then turns an honest unknown into a duplicate SMS.

Cross-repository contracts

This repository owns the two contract files. The relay keeps vendored copies:

  • agent-policy.json — the relay derives its envelope ceiling, mailbox depth, request deadline and long-poll window from these numbers, so the two cannot drift into disagreeing about what a node will accept. scripts/sync-contracts.sh in the relay repository pulls the copy.
  • test-vectors.json — generated here by make vectors (or go run ./cmd/msa-vectors), recomputed by the relay's Node reference implementation and by the app's Swift tests.

Drift in the policy is visible at runtime rather than only in a test: the agent sends its policy digest in the handshake, the relay compares it with its own copy and reports policyMatches per node in /v1/status.

License

No license file yet: until one is added, the default is all rights reserved. The source is public so the binaries can be fetched by a node that has no credentials — the install command downloads from this repository's releases — not as an invitation to redistribute.

S
Description
Outbound-only node agent for SimAdmin nodes: long-polls a relay, verifies signed envelopes, and forwards to the local SimAdmin on loopback. Static Go binary, no third-party modules.
Readme
268 KiB
2026-09-18 12:39:58 +08:00
Languages
Go 96.8%
Shell 2.4%
Makefile 0.8%