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 按子目录抽取)。
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# Cross-compiled binaries. The build is reproducible from the Makefile, so committing multi-megabyte
|
||||||
|
# ELF files would only make the repository grow with every rebuild.
|
||||||
|
dist/
|
||||||
|
# The vendored copy is produced by `make vendor` from contracts/agent-policy.json.
|
||||||
|
internal/policy/agent-policy.json.tmp
|
||||||
|
internal/install/msa-agent.service.tmp
|
||||||
|
# A binary left behind by `go build ./...` run in this directory.
|
||||||
|
msa-agent
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
@@ -1,21 +1,31 @@
|
|||||||
POLICY_SRC := ../contracts/agent-policy.json
|
# This repository owns the authorisation surface: the agent is what enforces it, so it is not a copy of
|
||||||
|
# a file that lives somewhere else. `contracts/agent-policy.json` is embedded in the binary and is the
|
||||||
|
# source of truth; the relay vendors a copy and the two are compared at runtime, by the policy digest the
|
||||||
|
# agent reports in its handshake.
|
||||||
|
POLICY_SRC := contracts/agent-policy.json
|
||||||
POLICY_DST := internal/policy/agent-policy.json
|
POLICY_DST := internal/policy/agent-policy.json
|
||||||
# The systemd unit is embedded in the binary so that `msa-agent install` needs no route to anything but
|
# The systemd unit is embedded in the binary so that `msa-agent install` needs no route to anything but
|
||||||
# the relay, and so the unit a node runs is the one its agent was built with. Vendored mechanically for
|
# the relay, and so the unit a node runs is the one its agent was built with. Vendored mechanically: an
|
||||||
# the same reason the policy is: an operator editing the copy under internal/ would be editing a file
|
# operator editing the copy under internal/ would be editing a file the build overwrites, and
|
||||||
# the build overwrites, and TestVendoredUnitMatchesSystemd makes that loud.
|
# TestVendoredUnitMatchesSystemd makes that loud.
|
||||||
UNIT_SRC := systemd/msa-agent.service
|
UNIT_SRC := systemd/msa-agent.service
|
||||||
UNIT_DST := internal/install/msa-agent.service
|
UNIT_DST := internal/install/msa-agent.service
|
||||||
# The version stamped into the binary, and the tag scripts/release.sh publishes under. It is the
|
# The version stamped into the binary, and the tag scripts/release.sh publishes under. It is the
|
||||||
# agent's own version rather than the app's: the two are released together but a node can be running an
|
# agent's own version rather than the app's: the two are released together but a node can be running an
|
||||||
# older agent than the phone, and the handshake's `agentBuild` field is what makes that visible.
|
# older agent than the phone, and the handshake's `agentBuild` field is what makes that visible.
|
||||||
VERSION ?= 0.3.0
|
VERSION ?= 1.0.0
|
||||||
LDFLAGS := -s -w -X main.version=$(VERSION)
|
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||||
|
|
||||||
.PHONY: all vendor test vet build build-arm64 build-armv7 clean
|
.PHONY: all vendor test vet fmt vectors build build-arm64 build-armv7 clean
|
||||||
|
|
||||||
all: vet test build
|
all: vet test build
|
||||||
|
|
||||||
|
# Regenerates contracts/test-vectors.json. It is the Go implementation that generates them, and the
|
||||||
|
# relay's Node reference implementation and the app's Swift tests recompute them from the result — three
|
||||||
|
# implementations with no shared code, so agreement is evidence rather than a tautology.
|
||||||
|
vectors:
|
||||||
|
@go run ./cmd/msa-vectors contracts/test-vectors.json
|
||||||
|
|
||||||
# The policy is embedded, so the vendored copy is part of the binary. Keep the copy mechanical:
|
# The policy is embedded, so the vendored copy is part of the binary. Keep the copy mechanical:
|
||||||
# an operator editing internal/policy/agent-policy.json by hand would be editing a file the build
|
# an operator editing internal/policy/agent-policy.json by hand would be editing a file the build
|
||||||
# overwrites, and the drift test makes that mistake loud rather than quiet.
|
# overwrites, and the drift test makes that mistake loud rather than quiet.
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
```text
|
||||||
|
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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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.
|
||||||
@@ -144,7 +144,9 @@ func main() {
|
|||||||
check(err)
|
check(err)
|
||||||
encoded = append(encoded, '\n')
|
encoded = append(encoded, '\n')
|
||||||
|
|
||||||
target := filepath.Join("..", "..", "contracts", "test-vectors.json")
|
// This repository owns the vectors: the Go implementation generates them, and the Node and Swift
|
||||||
|
// implementations recompute them from a vendored copy.
|
||||||
|
target := filepath.Join("contracts", "test-vectors.json")
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
target = os.Args[1]
|
target = os.Args[1]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "msa-agent-policy",
|
||||||
|
"comment": "Authorisation surface for relay-issued commands. Deliberately narrower than the web console proxy policy: this is what the iPhone app can actually ask for, nothing more.",
|
||||||
|
"egress": {
|
||||||
|
"mode": "loopback-only",
|
||||||
|
"portOnly": true,
|
||||||
|
"comment": "The agent config carries a port, never a host. Outbound connections are dialed to 127.0.0.1/::1 by the transport itself, so there is no host field to misconfigure and no DNS-rebinding path.",
|
||||||
|
"dialedHosts": ["127.0.0.1", "::1"]
|
||||||
|
},
|
||||||
|
"authPaths": {
|
||||||
|
"pattern": "^/api/(?:auth(?:/|$)|login(?:/|$)|logout(?:/|$))",
|
||||||
|
"proxied": false,
|
||||||
|
"comment": "The agent logs into the local SimAdmin itself and holds the credential. Login material never traverses the relay in either direction."
|
||||||
|
},
|
||||||
|
"read": [
|
||||||
|
"/api/health",
|
||||||
|
"/api/device",
|
||||||
|
"/api/sim",
|
||||||
|
"/api/network",
|
||||||
|
"/api/network/signal-strength",
|
||||||
|
"/api/stats",
|
||||||
|
"/api/stats/cpu",
|
||||||
|
"/api/connectivity",
|
||||||
|
"/api/sms/list"
|
||||||
|
],
|
||||||
|
"write": {
|
||||||
|
"/api/sms/send": ["POST"],
|
||||||
|
"/api/sms/batch-delete": ["POST"]
|
||||||
|
},
|
||||||
|
"dangerous": ["/api/sms/send", "/api/sms/batch-delete"],
|
||||||
|
"limits": {
|
||||||
|
"dangerousWritesPerMinute": 6,
|
||||||
|
"agentUpstreamTimeoutSeconds": 20,
|
||||||
|
"maxRequestBytes": 262144,
|
||||||
|
"maxResponseBytes": 262144,
|
||||||
|
"requestTtlSeconds": 30,
|
||||||
|
"clockSkewSeconds": 120,
|
||||||
|
"replayCacheSize": 4096,
|
||||||
|
"mailboxDepth": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"generatedBy": "agent/cmd/msa-vectors",
|
||||||
|
"note": "Deterministic cross-language vectors. The Node relay (test/wire.test.js) and the Swift app recompute these from this file; a mismatch means the envelope encoding drifted, not that one side is broken.",
|
||||||
|
"suite": "X25519-ED25519-AES256GCM-HKDFSHA256",
|
||||||
|
"version": 1,
|
||||||
|
"canonical": {
|
||||||
|
"comment": "Length-prefixed canonical bytes, then the deterministic Ed25519 signature over them. ek and p are raw decoded bytes, never their base64 text.",
|
||||||
|
"input": {
|
||||||
|
"v": 1,
|
||||||
|
"node": "nd_7f3a9c2e",
|
||||||
|
"req": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
||||||
|
"app": "ap_5b1d",
|
||||||
|
"ts": 1727289600,
|
||||||
|
"ek": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=",
|
||||||
|
"alg": "X25519-ED25519-AES256GCM-HKDFSHA256",
|
||||||
|
"p": "AAEC/v8="
|
||||||
|
},
|
||||||
|
"bytes": {
|
||||||
|
"request": "6d73612d72656c61792d76312d726571000000000800000000000000010000000b6e645f37663361396332650000002433663235303465302d346638392d343164332d396130632d3033303565383263333330310000000761705f35623164000000080000000066f45900000000208520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a00000005000102feff",
|
||||||
|
"response": "6d73612d72656c61792d76312d72657370000000000800000000000000010000000b6e645f37663361396332650000002433663235303465302d346638392d343164332d396130632d303330356538326333333031000000080000000066f45900000000208520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a00000005000102feff"
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"request": "f91fc4a40c4cb19643515894dba14a1baae602d0b307feafcf441a2d64690b94ac10e20982f8f3e8d2b48c74db11d250344256af92f7c22fa1b2c2fb6975fb0f",
|
||||||
|
"response": "3977dc4ad544ed83d9921be9929c49f5b38cd618583d99d10d096d3997237c02110da17ffebd45656453d94cd07044d2b1348e0c45381fa193bf5fb08b30b50e"
|
||||||
|
},
|
||||||
|
"publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||||||
|
"responsePublicKey": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"
|
||||||
|
},
|
||||||
|
"hkdf": {
|
||||||
|
"comment": "RFC 5869 test case 1, pinning the KDF itself rather than only its callers.",
|
||||||
|
"secret": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
|
||||||
|
"salt": "000102030405060708090a0b0c",
|
||||||
|
"info": "f0f1f2f3f4f5f6f7f8f9",
|
||||||
|
"length": 42,
|
||||||
|
"output": "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
|
||||||
|
},
|
||||||
|
"x25519": {
|
||||||
|
"comment": "RFC 7748 section 6.1, then the derived per-direction key and IV for the envelope above. The IV is bound to the request id and the direction, so a key that is ever reused still cannot reuse a nonce.",
|
||||||
|
"appPrivate": "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a",
|
||||||
|
"agentPublic": "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
|
||||||
|
"sharedSecret": "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742",
|
||||||
|
"requestKey": "14f4f1823bd68aabccf326e5a4ee69891428491477bfc6949a25dd69bff1b1e9",
|
||||||
|
"requestIV": "e5be943a5b8ea89e34bbcb02",
|
||||||
|
"responseKey": "e350511cf24be4045c770180276146ec647283695394dec608f1695b5f07524e",
|
||||||
|
"responseIV": "44c5c0499337c5da62b463ad"
|
||||||
|
},
|
||||||
|
"aead": {
|
||||||
|
"comment": "AES-256-GCM under the request key above; the 16-byte tag is appended.",
|
||||||
|
"key": "14f4f1823bd68aabccf326e5a4ee69891428491477bfc6949a25dd69bff1b1e9",
|
||||||
|
"iv": "e5be943a5b8ea89e34bbcb02",
|
||||||
|
"plaintext": "7b226d223a22474554222c2270223a222f6170692f6865616c7468227d",
|
||||||
|
"ciphertext": "6601340c304905a8308894ab9d050d5e2ea083ee7ad43df01b9c3e79daead5321099e7203abd5985cf2ef12114"
|
||||||
|
},
|
||||||
|
"intent": {
|
||||||
|
"comment": "sha256(method | path | canonical body). The agent recomputes it from the request it is about to forward, so a relay cannot edit a confirmed write.",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/sms/send",
|
||||||
|
"body": "{\"phone_number\":\"+8613800000000\",\"content\":\"hello\"}",
|
||||||
|
"digest": "d0f584d844cf6e25d5b233a65e86fd80b82a396836cb80522e528b0f6ea5ac00"
|
||||||
|
},
|
||||||
|
"canonicalJson": [
|
||||||
|
{
|
||||||
|
"input": "{\"b\":1,\"a\":2}",
|
||||||
|
"output": "{\"a\":2,\"b\":1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "[3,1,2]",
|
||||||
|
"output": "[3,1,2]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": " null ",
|
||||||
|
"output": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "true",
|
||||||
|
"output": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "\"\u003c\u0026\u003e\"",
|
||||||
|
"output": "\"\u003c\u0026\u003e\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"x\":100}",
|
||||||
|
"output": "{\"x\":100}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"a\":{\"c\":[1,{\"b\":2,\"a\":1}],\"b\":0},\"A\":9}",
|
||||||
|
"output": "{\"A\":9,\"a\":{\"b\":0,\"c\":[1,{\"a\":1,\"b\":2}]}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"phone_number\":\"+8613800000000\",\"content\":\"hello\"}",
|
||||||
|
"output": "{\"content\":\"hello\",\"phone_number\":\"+8613800000000\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"content\":\"a\u2028b\"}",
|
||||||
|
"output": "{\"content\":\"a\u2028b\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"content\":\"a\u2029b\"}",
|
||||||
|
"output": "{\"content\":\"a\u2029b\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"content\":\"😀\"}",
|
||||||
|
"output": "{\"content\":\"😀\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"content\":\"\\b\\f\\u0001\"}",
|
||||||
|
"output": "{\"content\":\"\\b\\f\\u0001\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"content\":\"\\\\u2028\"}",
|
||||||
|
"output": "{\"content\":\"\\\\u2028\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"é\":1,\"z\":2}",
|
||||||
|
"output": "{\"z\":2,\"é\":1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "{\"𐀀\":1,\"�\":2}",
|
||||||
|
"output": "{\"�\":2,\"𐀀\":1}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"canonicalJsonRejected": [
|
||||||
|
"{\"content\":\"\\ud800\"}",
|
||||||
|
"{\"content\":\"\\udc00\"}",
|
||||||
|
"{\"content\":\"\\ud800\\ud800\"}",
|
||||||
|
"{\"content\":\"\\ud800x\"}"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=MultiSimAdmin relay agent
|
Description=MultiSimAdmin relay agent
|
||||||
Documentation=https://gitea.chickliu.fun/Hermes/simadmin-relay
|
Documentation=https://gitea.chickliu.fun/Hermes/msa-agent
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ import (
|
|||||||
// TestVendoredCopyMatchesContracts fails loudly when the embedded policy and contracts/ drift apart.
|
// TestVendoredCopyMatchesContracts fails loudly when the embedded policy and contracts/ drift apart.
|
||||||
// Without this the shipped binary can enforce something other than what the tests in test/ checked,
|
// Without this the shipped binary can enforce something other than what the tests in test/ checked,
|
||||||
// which is exactly the silent-divergence case embedding was supposed to avoid.
|
// which is exactly the silent-divergence case embedding was supposed to avoid.
|
||||||
|
//
|
||||||
|
// The file this reads is the one this repository owns. The relay keeps its own copy — it derives its
|
||||||
|
// limits from the same numbers — and the two are compared at runtime by the policy digest the agent
|
||||||
|
// reports in its handshake, which the relay surfaces per node. A build-time comparison across two
|
||||||
|
// repositories is not something either side can make, so the guard is the digest and the drift is
|
||||||
|
// visible in /v1/status rather than in a test.
|
||||||
func TestVendoredCopyMatchesContracts(t *testing.T) {
|
func TestVendoredCopyMatchesContracts(t *testing.T) {
|
||||||
upstream, err := os.ReadFile(filepath.Join("..", "..", "..", "contracts", "agent-policy.json"))
|
upstream, err := os.ReadFile(filepath.Join("..", "..", "contracts", "agent-policy.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read contracts/agent-policy.json: %v", err)
|
t.Fatalf("read contracts/agent-policy.json: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=MultiSimAdmin relay agent
|
Description=MultiSimAdmin relay agent
|
||||||
Documentation=https://gitea.chickliu.fun/Hermes/simadmin-relay
|
Documentation=https://gitea.chickliu.fun/Hermes/msa-agent
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user