commit ded5c32f19d3f8e4ece5c6111bdb3d5238bbe0b3
parent ddfc4b1719b7dbc0bbd5eec5d6697d10b44a570e
Author: triesap <tyson@radroots.org>
Date: Sun, 23 Aug 2026 20:22:40 +0000
refactor(rhi): remove prototype runtime
Diffstat:
17 files changed, 198 insertions(+), 2575 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -32,8 +32,9 @@
## 2. Authority and preflight
- Before editing, read this file, `README`, `Cargo.toml`,
- `radroots.service.source-lock.v2.toml`, the relevant implementation and tests,
- and `config.toml` when it is in scope.
+ `radroots.service.source-lock.v2.toml`, and the relevant implementation and
+ tests. The removed prototype root `config.toml` is not configuration
+ authority.
- `.radroots-consumer-root` is the standalone source-lock identity and must
remain exactly `rhi`. The reserved pre-implementation evidence authority is
`contracts/services_hardening/evidence_policy.v1.json`, and the reserved
diff --git a/Cargo.lock b/Cargo.lock
@@ -88,7 +88,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -99,7 +99,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -115,37 +115,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
-name = "async-utility"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
-dependencies = [
- "futures-util",
- "gloo-timers",
- "tokio",
- "wasm-bindgen-futures",
-]
-
-[[package]]
-name = "async-wsocket"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
-dependencies = [
- "async-utility",
- "futures",
- "futures-util",
- "js-sys",
- "tokio",
- "tokio-rustls",
- "tokio-socks",
- "tokio-tungstenite",
- "url",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -155,12 +124,6 @@ dependencies = [
]
[[package]]
-name = "atomic-destructor"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
-
-[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -424,15 +387,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
name = "crossbeam-queue"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -487,15 +441,6 @@ dependencies = [
]
[[package]]
-name = "deranged"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
-dependencies = [
- "powerfmt",
-]
-
-[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -571,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -831,18 +776,6 @@ dependencies = [
]
[[package]]
-name = "gloo-timers"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
-dependencies = [
- "futures-channel",
- "futures-core",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1302,21 +1235,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
-name = "lru"
-version = "0.16.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
-
-[[package]]
-name = "matchers"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
-dependencies = [
- "regex-automata",
-]
-
-[[package]]
name = "mediatype"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1342,16 +1260,10 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
-name = "negentropy"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
-
-[[package]]
name = "nostr"
version = "0.44.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1378,68 +1290,6 @@ dependencies = [
]
[[package]]
-name = "nostr-database"
-version = "0.44.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
-dependencies = [
- "lru",
- "nostr",
- "tokio",
-]
-
-[[package]]
-name = "nostr-gossip"
-version = "0.44.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
-dependencies = [
- "nostr",
-]
-
-[[package]]
-name = "nostr-relay-pool"
-version = "0.44.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1"
-dependencies = [
- "async-utility",
- "async-wsocket",
- "atomic-destructor",
- "hex",
- "lru",
- "negentropy",
- "nostr",
- "nostr-database",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "nostr-sdk"
-version = "0.44.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
-dependencies = [
- "async-utility",
- "nostr",
- "nostr-database",
- "nostr-gossip",
- "nostr-relay-pool",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "nu-ansi-term"
-version = "0.50.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
-[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1479,12 +1329,6 @@ dependencies = [
]
[[package]]
-name = "num-conv"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
-
-[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1636,12 +1480,6 @@ dependencies = [
]
[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2014,13 +1852,11 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
name = "rhi"
version = "0.1.0"
dependencies = [
- "anyhow",
"chacha20poly1305",
"clap",
"futures-executor",
"jsonschema",
"nostr",
- "nostr-sdk",
"radroots_event",
"radroots_event_codec",
"radroots_identity",
@@ -2037,30 +1873,12 @@ dependencies = [
"sha2",
"tempfile",
"thiserror 2.0.18",
- "tokio",
"toml",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
"url",
"zeroize",
]
[[package]]
-name = "ring"
-version = "0.17.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
-dependencies = [
- "cc",
- "cfg-if",
- "getrandom 0.2.17",
- "libc",
- "untrusted",
- "windows-sys 0.52.0",
-]
-
-[[package]]
name = "rust_decimal"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2081,41 +1899,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "rustls"
-version = "0.23.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
-dependencies = [
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
-dependencies = [
- "zeroize",
-]
-
-[[package]]
-name = "rustls-webpki"
-version = "0.103.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
+ "windows-sys",
]
[[package]]
@@ -2243,17 +2027,6 @@ dependencies = [
]
[[package]]
-name = "sha1"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2265,31 +2038,12 @@ dependencies = [
]
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
-dependencies = [
- "errno",
- "libc",
-]
-
-[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2308,7 +2062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2506,7 +2260,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2550,46 +2304,6 @@ dependencies = [
]
[[package]]
-name = "thread_local"
-version = "1.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "time"
-version = "0.3.47"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde_core",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
-
-[[package]]
-name = "time-macros"
-version = "0.2.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
-[[package]]
name = "tinystr"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2623,12 +2337,10 @@ dependencies = [
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
"tokio-macros",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
@@ -2643,28 +2355,6 @@ dependencies = [
]
[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
-dependencies = [
- "rustls",
- "tokio",
-]
-
-[[package]]
-name = "tokio-socks"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
-dependencies = [
- "either",
- "futures-util",
- "thiserror 1.0.69",
- "tokio",
-]
-
-[[package]]
name = "tokio-stream"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2676,22 +2366,6 @@ dependencies = [
]
[[package]]
-name = "tokio-tungstenite"
-version = "0.26.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
-dependencies = [
- "futures-util",
- "log",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tungstenite",
- "webpki-roots 0.26.11",
-]
-
-[[package]]
name = "tokio-util"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2759,18 +2433,6 @@ dependencies = [
]
[[package]]
-name = "tracing-appender"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
-dependencies = [
- "crossbeam-channel",
- "thiserror 2.0.18",
- "time",
- "tracing-subscriber",
-]
-
-[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2788,36 +2450,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-subscriber"
-version = "0.3.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex-automata",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
]
[[package]]
@@ -2827,25 +2459,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "tungstenite"
-version = "0.26.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
-dependencies = [
- "bytes",
- "data-encoding",
- "http",
- "httparse",
- "log",
- "rand 0.9.2",
- "rustls",
- "rustls-pki-types",
- "sha1",
- "thiserror 2.0.18",
- "utf-8",
-]
-
-[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2895,12 +2508,6 @@ dependencies = [
]
[[package]]
-name = "untrusted"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-
-[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2926,12 +2533,6 @@ dependencies = [
]
[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
-[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2954,12 +2555,6 @@ dependencies = [
]
[[package]]
-name = "valuable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-
-[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3024,20 +2619,6 @@ dependencies = [
]
[[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.64"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
-dependencies = [
- "cfg-if",
- "futures-util",
- "js-sys",
- "once_cell",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3114,24 +2695,6 @@ dependencies = [
]
[[package]]
-name = "webpki-roots"
-version = "0.26.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
-dependencies = [
- "webpki-roots 1.0.6",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3161,15 +2724,6 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
@@ -3178,70 +2732,6 @@ dependencies = [
]
[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2024"
authors = ["Radroots Authors"]
rust-version = "1.97.1"
license = "AGPL-3.0-or-later"
-description = "Radroots trade agreement attestation worker"
+description = "Radroots evidence reconciliation and attestation service"
repository = "https://github.com/radrootslabs/rhi"
readme = "README"
publish = false
@@ -54,24 +54,18 @@ radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = "
radroots_secrets = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
radroots_trade = { git = "https://github.com/radrootslabs/lib", rev = "7d7b454b4c9ed86569671993bd03ca868b676665", version = "=0.1.0-alpha" }
-anyhow = { version = "1" }
chacha20poly1305 = { version = "0.10" }
clap = { version = "4", features = ["derive"] }
futures-executor = { version = "0.3" }
jsonschema = { version = "0.48.1", default-features = false }
nostr = { version = "0.44.7", features = ["nip49"] }
-nostr-sdk = { version = "0.44.1" }
rand = { version = "0.9" }
serde = { version = "1", default-features = false }
serde_json = { version = "1", default-features = false }
sha2 = { version = "0.10" }
-tokio = { version = "1", features = ["full"] }
thiserror = { version = "2" }
tempfile = { version = "3" }
toml = { version = "0.8" }
-tracing = { version = "0.1" }
-tracing-appender = { version = "0.2" }
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
zeroize = { version = "1" }
diff --git a/README b/README
@@ -20,9 +20,12 @@ reviewed bounded operational leaves have defaults. Bootstrap profile,
instance, repo-local root, and config-path selection are CLI concerns and are
not document fields.
-The current runtime loader and root `config.toml` remain transitional until
-their ordered replacement steps are complete. Prototype environment and worker
-selectors are removed and are not compatibility authority.
+The retired root `config.toml`, transitional runtime loader, JSON state
+adapter, environment and worker selectors, and prototype smoke/runtime paths
+are removed. The executable admits one strict CLI invocation and one sealed
+runtime context, then fails closed until later ordered checkpoints bind each
+command to its governed state and runtime authority. It never falls back to a
+prototype execution path.
`parse_rhi_config_v1` is the strict in-memory admission boundary for the target
document. It bounds original bytes before parsing, rejects malformed duplicate
diff --git a/config.toml b/config.toml
@@ -1,31 +0,0 @@
-[metadata]
-name = "rhi"
-
-[logging]
-filter = "info"
-stdout = true
-
-[relays]
-urls = [
- "ws://127.0.0.1:8080"
-]
-
-[nostr.nip89]
-identifier = "rhi"
-extra_tags = []
-
-[subscriber.backoff]
-base_ms = 500
-max_ms = 30000
-factor = 2
-jitter_ms = 0
-
-[subscriber.state]
-replay_window_secs = 86400
-replay_overlap_secs = 300
-
-[trade_validation_receipt]
-backend = "local_execute"
-proof_mode = "none"
-validator_set_addr = "30381:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:018f3d99-7d35-7c0c-8a0f-7f3b645abcde"
-validator_set_event_id = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
diff --git a/radroots.service.source-lock.v2.toml b/radroots.service.source-lock.v2.toml
@@ -7,7 +7,7 @@ architecture = "radroots.crates.release.v2"
workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4"
version = "0.1.0-alpha"
source_archive_sha256 = "b425371c134be96cce46b37f7035d6212f1efe8cff50bef366631ba5632991b0"
-cargo_lock_sha256 = "ba1cca624b0b2fbc49e82bc392b4cc1bf80a9ffcb5bd86e15ffed2818075f565"
+cargo_lock_sha256 = "407af5f68ddb487db8867bb84e20c8a4645351c96c3695ae41973c4ea4b44a12"
rust_version = "1.97.1"
host_feature_profile = "service-host"
diff --git a/src/adapters/nostr/event.rs b/src/adapters/nostr/event.rs
@@ -1,4 +1,4 @@
-use crate::host_nostr::{Event, Kind};
+use nostr::{Event, Kind};
use radroots_event_codec::decode::job::{JobEventBorrow, JobEventLike};
#[derive(Clone, Debug)]
@@ -81,8 +81,9 @@ impl JobEventLike for NostrEventAdapter<'_> {
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::NostrEventAdapter;
- use crate::host_nostr::{Event, GenericBuilder, Keys, Kind, Tag, TagKind};
+ use nostr::{Event, Keys, Kind, Tag, TagKind};
use radroots_event_codec::decode::job::{JobEventBorrow, JobEventLike};
+ use radroots_nostr::event::GenericBuilder;
fn build_event(keys: &Keys, kind: Kind, tags: Vec<Tag>) -> Event {
GenericBuilder::new(kind, "content")
diff --git a/src/config.rs b/src/config.rs
@@ -1,314 +0,0 @@
-//! Transitional runtime settings materialization under the sealed path context.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::{Context, Result, bail};
-use serde::{Deserialize, Serialize};
-
-use crate::RhiRuntimeContext;
-use crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy;
-use crate::host_nostr::Metadata;
-use crate::host_runtime::{BackoffConfig, NostrServiceConfig};
-
-fn default_replay_window_secs() -> u64 {
- 24 * 60 * 60
-}
-
-fn default_replay_overlap_secs() -> u64 {
- 5 * 60
-}
-
-fn default_logging_filter() -> String {
- "info".to_owned()
-}
-
-const fn default_logging_stdout() -> bool {
- true
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct LoggingConfig {
- pub output_dir: PathBuf,
- pub filter: String,
- pub stdout: bool,
-}
-
-#[derive(Debug, Deserialize, Clone, Default)]
-#[serde(default, deny_unknown_fields)]
-struct RawLoggingConfig {
- filter: Option<String>,
- stdout: Option<bool>,
-}
-
-impl RawLoggingConfig {
- fn into_logging_config(self, context: &RhiRuntimeContext) -> Result<LoggingConfig> {
- let filter = self.filter.unwrap_or_else(default_logging_filter);
- let filter = filter.trim();
- if filter.is_empty() {
- bail!("logging.filter must not be empty");
- }
- Ok(LoggingConfig {
- output_dir: context.context().paths().logs().to_path_buf(),
- filter: filter.to_owned(),
- stdout: self.stdout.unwrap_or_else(default_logging_stdout),
- })
- }
-}
-
-#[derive(Debug, Deserialize, Clone, Default)]
-#[serde(default, deny_unknown_fields)]
-struct RawRelaysConfig {
- urls: Vec<String>,
-}
-
-#[derive(Debug, Deserialize, Clone, Default)]
-#[serde(default, deny_unknown_fields)]
-struct RawNostrConfig {
- nip89: RawNip89Config,
-}
-
-#[derive(Debug, Deserialize, Clone, Default)]
-#[serde(default, deny_unknown_fields)]
-struct RawNip89Config {
- identifier: Option<String>,
- extra_tags: Vec<Vec<String>>,
-}
-
-#[derive(Debug, Clone)]
-struct RawServiceConfig {
- logging: LoggingConfig,
- relays: RawRelaysConfig,
- nostr: RawNostrConfig,
-}
-
-impl RawServiceConfig {
- fn into_service_config(self) -> NostrServiceConfig {
- NostrServiceConfig {
- logs_dir: self.logging.output_dir.display().to_string(),
- relays: self.relays.urls,
- nip89_identifier: self.nostr.nip89.identifier,
- nip89_extra_tags: self.nostr.nip89.extra_tags,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Configuration {
- #[serde(flatten)]
- pub service: NostrServiceConfig,
- pub logging: LoggingConfig,
- pub subscriber: SubscriberConfig,
- #[serde(default)]
- pub trade_agreement_attestation: TradeAgreementAttestationPolicy,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct SubscriberConfig {
- pub backoff: BackoffConfig,
- pub state: SubscriberStateConfig,
-}
-
-#[derive(Debug, Deserialize, Clone, Default)]
-#[serde(default, deny_unknown_fields)]
-struct RawSubscriberConfig {
- backoff: BackoffConfig,
- state: RawSubscriberStateConfig,
-}
-
-impl RawSubscriberConfig {
- fn into_subscriber_config(self, context: &RhiRuntimeContext) -> SubscriberConfig {
- SubscriberConfig {
- backoff: self.backoff,
- state: self.state.into_subscriber_state_config(context),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct SubscriberStateConfig {
- pub path: PathBuf,
- pub replay_window_secs: u64,
- pub replay_overlap_secs: u64,
-}
-
-#[derive(Debug, Deserialize, Clone)]
-#[serde(deny_unknown_fields)]
-struct RawSubscriberStateConfig {
- #[serde(default = "default_replay_window_secs")]
- replay_window_secs: u64,
- #[serde(default = "default_replay_overlap_secs")]
- replay_overlap_secs: u64,
-}
-
-impl Default for RawSubscriberStateConfig {
- fn default() -> Self {
- Self {
- replay_window_secs: default_replay_window_secs(),
- replay_overlap_secs: default_replay_overlap_secs(),
- }
- }
-}
-
-impl RawSubscriberStateConfig {
- fn into_subscriber_state_config(self, context: &RhiRuntimeContext) -> SubscriberStateConfig {
- SubscriberStateConfig {
- path: context
- .context()
- .paths()
- .state()
- .join("trade-agreement-attestation")
- .join("state.json"),
- replay_window_secs: self.replay_window_secs,
- replay_overlap_secs: self.replay_overlap_secs,
- }
- }
-}
-
-#[derive(Debug, Deserialize, Clone)]
-#[serde(deny_unknown_fields)]
-struct RawSettings {
- metadata: Metadata,
- #[serde(default)]
- logging: RawLoggingConfig,
- #[serde(default)]
- relays: RawRelaysConfig,
- #[serde(default)]
- nostr: RawNostrConfig,
- #[serde(default)]
- subscriber: RawSubscriberConfig,
- #[serde(default)]
- trade_agreement_attestation: TradeAgreementAttestationPolicy,
-}
-
-impl RawSettings {
- fn into_settings(self, context: &RhiRuntimeContext) -> Result<Settings> {
- let logging = self.logging.into_logging_config(context)?;
- let service = RawServiceConfig {
- logging: logging.clone(),
- relays: self.relays,
- nostr: self.nostr,
- }
- .into_service_config();
- Ok(Settings {
- metadata: self.metadata,
- config: Configuration {
- service,
- logging,
- subscriber: self.subscriber.into_subscriber_config(context),
- trade_agreement_attestation: self.trade_agreement_attestation,
- },
- })
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Settings {
- pub metadata: Metadata,
- pub config: Configuration,
-}
-
-/// Loads transitional runtime settings with all paths supplied by one sealed context.
-///
-/// The final versioned configuration parser is [`crate::parse_rhi_config_v1`].
-/// This adapter remains only until the legacy runtime is removed in Step 168;
-/// it accepts no path overrides and performs no ambient environment selection.
-pub fn load_settings_from_path(path: &Path, context: &RhiRuntimeContext) -> Result<Settings> {
- let raw = std::fs::read_to_string(path)
- .with_context(|| format!("read configuration from {}", path.display()))?;
- let settings: RawSettings =
- toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?;
- let settings = settings.into_settings(context)?;
- settings.config.trade_agreement_attestation.validate()?;
- Ok(settings)
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::{Path, PathBuf};
-
- use crate::{
- RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from,
- resolve_rhi_runtime_context,
- };
-
- use super::load_settings_from_path;
-
- fn context() -> crate::RhiRuntimeContext {
- let invocation = parse_rhi_cli_v1_from([
- "rhi",
- "--profile",
- "interactive",
- "--instance",
- "default",
- "run",
- ])
- .expect("invocation");
- resolve_rhi_runtime_context(
- &RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/operator")),
- xdg_config_home: Some(PathBuf::from("/xdg/config")),
- xdg_data_home: Some(PathBuf::from("/xdg/data")),
- xdg_state_home: Some(PathBuf::from("/xdg/state")),
- xdg_cache_home: Some(PathBuf::from("/xdg/cache")),
- xdg_runtime_dir: Some(PathBuf::from("/xdg/run")),
- ..RadrootsHostEnvironment::default()
- },
- ),
- &invocation,
- )
- .expect("context")
- }
-
- #[test]
- fn materializes_only_context_derived_paths() {
- let temp = tempfile::tempdir().expect("tempdir");
- let config_path = temp.path().join("config.toml");
- std::fs::write(
- &config_path,
- r#"
-[metadata]
-name = "rhi-test"
-
-[relays]
-urls = ["wss://relay.example.com"]
-
-[subscriber.state]
-replay_window_secs = 123
-replay_overlap_secs = 45
-"#,
- )
- .expect("config");
- let settings = load_settings_from_path(&config_path, &context()).expect("settings");
- assert_eq!(
- settings.config.logging.output_dir,
- Path::new("/xdg/state/radroots/logs/services/rhi/default")
- );
- assert_eq!(
- settings.config.subscriber.state.path,
- Path::new(
- "/xdg/data/radroots/services/rhi/default/trade-agreement-attestation/state.json"
- )
- );
- assert_eq!(settings.config.subscriber.state.replay_window_secs, 123);
- assert_eq!(settings.config.subscriber.state.replay_overlap_secs, 45);
- }
-
- #[test]
- fn path_leaf_overrides_are_rejected() {
- let temp = tempfile::tempdir().expect("tempdir");
- for (name, extra) in [
- ("logging", "[logging]\noutput_dir = \"/tmp/logs\"\n"),
- ("state", "[subscriber.state]\npath = \"/tmp/state.json\"\n"),
- ] {
- let config_path = temp.path().join(format!("{name}.toml"));
- std::fs::write(
- &config_path,
- format!("[metadata]\nname = \"rhi-test\"\n\n{extra}"),
- )
- .expect("config");
- assert!(load_settings_from_path(&config_path, &context()).is_err());
- }
- }
-}
diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs
@@ -1,41 +1,19 @@
#![forbid(unsafe_code)]
#![cfg_attr(coverage_nightly, coverage(off))]
-use std::collections::{BTreeMap, BTreeSet};
-use std::convert::TryFrom;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-use std::time::Duration;
-
-use crate::host_nostr::{
- Client, Event, Filter, Keys, Kind, RelayPoolNotification, SubscriptionId, Timestamp,
-};
-use anyhow::{Result, anyhow};
-use radroots_event::envelope::kind::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind};
-use radroots_event::id::{AddressableCoordinate, EventId, MutationId, TradeId};
-use radroots_event::trade::{
- TradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content,
-};
-use radroots_identity::PublicKey;
-use radroots_trade::evidence::{
- RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
-};
+use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS;
+use radroots_event::id::{AddressableCoordinate, EventId, MutationId};
+use radroots_event::trade::canonical_jcs_value;
+use radroots_trade::evidence::{RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1};
use radroots_trade::model::{
RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeProjectionV1,
};
-use radroots_trade::reducer::{
- RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
- RadrootsTradeReductionInputV1, reduce_trade_records,
-};
+use radroots_trade::reducer::{RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
-use tokio::sync::{Mutex, watch};
-use tokio::time::sleep;
-use tracing::{info, warn};
pub const RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID: &str = "radroots.rhi.agreement_attestation.v1";
-pub const RHI_AGREEMENT_ATTESTATION_STATE_VERSION: u32 = 1;
pub const RHI_AGREEMENT_ATTESTATION_REPORT_VERSION: u16 = 1;
pub const RHI_AGREEMENT_ATTESTATION_PROOF_SYSTEM_LOCAL_STATEMENT_HASH: &str =
"local_statement_hash";
@@ -155,63 +133,8 @@ pub struct TradeAgreementAttestationReportV1 {
pub proof_identity_hash: String,
}
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct TradeMutationObservationV1 {
- pub event_id: String,
- pub event_kind: u32,
- pub event_pubkey: String,
- pub trade_id: String,
- pub mutation_id: String,
- pub content: String,
- pub observed_at_unix_s: u64,
-}
-
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct TradeAgreementAttestationState {
- #[serde(default)]
- seen_event_ids: BTreeSet<String>,
- #[serde(default)]
- mutation_events: BTreeMap<String, TradeMutationObservationV1>,
- #[serde(default)]
- reports_by_claim: BTreeMap<String, TradeAgreementAttestationReportV1>,
- last_event_created_at: Option<u32>,
-}
-
-#[derive(Clone, Debug)]
-pub struct TradeAgreementAttestationRuntime {
- state: Arc<Mutex<TradeAgreementAttestationState>>,
- config: TradeAgreementAttestationRuntimeConfig,
- persistence: Option<Arc<TradeAgreementAttestationStatePersistence>>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct TradeAgreementAttestationRuntimeConfig {
- pub state_path: PathBuf,
- pub replay_window_secs: u64,
- pub replay_overlap_secs: u64,
-}
-
-#[derive(Clone, Debug)]
-struct TradeAgreementAttestationStatePersistence {
- path: PathBuf,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct PersistedTradeAgreementAttestationState {
- version: u32,
- state: TradeAgreementAttestationState,
-}
-
#[derive(Debug, Error)]
pub enum TradeAgreementAttestationError {
- #[error("event kind not supported")]
- UnsupportedKind,
- #[error("invalid event author")]
- InvalidEventAuthor,
- #[error("trade mutation is missing mutation_id")]
- MissingMutationId,
#[error("agreement claim is missing")]
MissingAgreementClaim,
#[error("attestation policy is missing {0}")]
@@ -220,309 +143,10 @@ pub enum TradeAgreementAttestationError {
InvalidValidatorSetBinding(&'static str),
#[error("invalid configured hash field")]
InvalidHashField,
- #[error("invalid signed event")]
- InvalidSignedEvent,
#[error("trade protocol error: {0}")]
TradeProtocol(#[from] radroots_event::trade::TradeProtocolError),
#[error("serde error: {0}")]
Serde(#[from] serde_json::Error),
- #[error("state error: {0}")]
- State(#[from] TradeAgreementAttestationRuntimeError),
-}
-
-#[derive(Debug, Error)]
-pub enum TradeAgreementAttestationRuntimeError {
- #[error("state path is invalid: {0}")]
- InvalidStatePath(PathBuf),
- #[error("state version {0} is unsupported")]
- UnsupportedStateVersion(u32),
- #[error("io error: {0}")]
- Io(#[from] std::io::Error),
- #[error("serde error: {0}")]
- Serde(#[from] serde_json::Error),
-}
-
-impl Default for TradeAgreementAttestationRuntimeConfig {
- fn default() -> Self {
- Self {
- // In-memory runtimes never construct persistence from this value.
- // Persistent runtimes must receive their context-derived path.
- state_path: PathBuf::new(),
- replay_window_secs: 24 * 60 * 60,
- replay_overlap_secs: 5 * 60,
- }
- }
-}
-
-impl Default for TradeAgreementAttestationRuntime {
- fn default() -> Self {
- Self {
- state: Arc::new(Mutex::new(TradeAgreementAttestationState::default())),
- config: TradeAgreementAttestationRuntimeConfig::default(),
- persistence: None,
- }
- }
-}
-
-impl TradeAgreementAttestationRuntime {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub async fn load(
- config: TradeAgreementAttestationRuntimeConfig,
- ) -> Result<Self, TradeAgreementAttestationRuntimeError> {
- let persistence = Arc::new(TradeAgreementAttestationStatePersistence::new(
- config.state_path.clone(),
- ));
- let state = persistence.load().await?;
- Ok(Self {
- state: Arc::new(Mutex::new(state)),
- config,
- persistence: Some(persistence),
- })
- }
-
- pub fn state(&self) -> Arc<Mutex<TradeAgreementAttestationState>> {
- Arc::clone(&self.state)
- }
-
- pub async fn persist(&self) -> Result<(), TradeAgreementAttestationRuntimeError> {
- let Some(persistence) = &self.persistence else {
- return Ok(());
- };
- let snapshot = self.state.lock().await.clone();
- persistence.persist(&snapshot).await
- }
-
- pub async fn mark_processed_event(
- &self,
- created_at: u32,
- ) -> Result<(), TradeAgreementAttestationRuntimeError> {
- {
- let mut state = self.state.lock().await;
- state.observe_event_created_at(created_at);
- }
- self.persist().await
- }
-
- pub async fn recovery_filter(&self, kinds: Vec<Kind>) -> Filter {
- let since = {
- let state = self.state.lock().await;
- state.replay_since(
- Timestamp::now().as_secs(),
- self.config.replay_window_secs,
- self.config.replay_overlap_secs,
- )
- };
- Filter::new().kinds(kinds).since(Timestamp::from(since))
- }
-
- pub async fn reports(&self) -> Vec<TradeAgreementAttestationReportV1> {
- self.state
- .lock()
- .await
- .reports_by_claim
- .values()
- .cloned()
- .collect()
- }
-
- async fn observe_mutation_event(
- &self,
- event: &Event,
- mutation: &TradeMutationEnvelopeV1,
- mutation_id: &MutationId,
- kind: u32,
- ) -> Result<bool, TradeAgreementAttestationRuntimeError> {
- let observation = TradeMutationObservationV1 {
- event_id: event.id.to_hex(),
- event_kind: kind,
- event_pubkey: event.pubkey.to_hex(),
- trade_id: mutation.trade_id.to_hex(),
- mutation_id: mutation_id.to_hex(),
- content: event.content.clone(),
- observed_at_unix_s: event.created_at.as_secs(),
- };
- let mut state = self.state.lock().await;
- if !state.seen_event_ids.insert(observation.event_id.clone()) {
- return Ok(false);
- }
- state
- .mutation_events
- .insert(observation.mutation_id.clone(), observation);
- Ok(true)
- }
-
- async fn reduce_trade(
- &self,
- trade_id: &TradeId,
- ) -> Result<RadrootsTradeProjectionV1, TradeAgreementAttestationError> {
- let observations = {
- let state = self.state.lock().await;
- state
- .mutation_events
- .values()
- .filter(|observation| observation.trade_id == trade_id.to_hex().as_str())
- .cloned()
- .collect::<Vec<_>>()
- };
- let mutations = observations
- .iter()
- .map(|observation| {
- let mutation = trade_mutation_from_canonical_content(observation.content.as_str())?;
- let transport_event_id = EventId::parse(observation.event_id.as_str())
- .map(Some)
- .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?;
- Ok(RadrootsTradeMutationRecordV1::new(
- transport_event_id,
- mutation,
- ))
- })
- .collect::<Result<Vec<_>, TradeAgreementAttestationError>>()?;
- let input = RadrootsTradeReductionInputV1::new(*trade_id)
- .with_evidence_state(RadrootsTradeEvidenceStateV1::Complete)
- .with_mutations(mutations);
- Ok(reduce_trade_records(input))
- }
-
- async fn store_report(
- &self,
- report: TradeAgreementAttestationReportV1,
- ) -> Result<(), TradeAgreementAttestationRuntimeError> {
- {
- let mut state = self.state.lock().await;
- state
- .reports_by_claim
- .insert(report.statement.claim_mutation_id.clone(), report);
- }
- self.persist().await
- }
-}
-
-impl TradeAgreementAttestationState {
- pub fn report_for_claim(
- &self,
- claim_mutation_id: &str,
- ) -> Option<&TradeAgreementAttestationReportV1> {
- self.reports_by_claim.get(claim_mutation_id)
- }
-
- pub fn observed_mutation_count(&self) -> usize {
- self.mutation_events.len()
- }
-
- fn observe_event_created_at(&mut self, created_at: u32) {
- self.last_event_created_at = Some(
- self.last_event_created_at
- .map_or(created_at, |current| current.max(created_at)),
- );
- }
-
- fn replay_since(&self, now: u64, replay_window_secs: u64, replay_overlap_secs: u64) -> u64 {
- let window_start = now.saturating_sub(replay_window_secs);
- match self.last_event_created_at {
- Some(last) => u64::from(last).saturating_sub(replay_overlap_secs),
- None => window_start,
- }
- }
-}
-
-impl TradeAgreementAttestationStatePersistence {
- fn new(path: PathBuf) -> Self {
- Self { path }
- }
-
- async fn load(
- &self,
- ) -> Result<TradeAgreementAttestationState, TradeAgreementAttestationRuntimeError> {
- if !self.path.exists() {
- return Ok(TradeAgreementAttestationState::default());
- }
- let payload = tokio::fs::read_to_string(&self.path).await?;
- let snapshot: PersistedTradeAgreementAttestationState = serde_json::from_str(&payload)?;
- if snapshot.version != RHI_AGREEMENT_ATTESTATION_STATE_VERSION {
- return Err(
- TradeAgreementAttestationRuntimeError::UnsupportedStateVersion(snapshot.version),
- );
- }
- Ok(snapshot.state)
- }
-
- async fn persist(
- &self,
- state: &TradeAgreementAttestationState,
- ) -> Result<(), TradeAgreementAttestationRuntimeError> {
- let parent = self.path.parent().ok_or_else(|| {
- TradeAgreementAttestationRuntimeError::InvalidStatePath(self.path.clone())
- })?;
- tokio::fs::create_dir_all(parent).await?;
- let snapshot = PersistedTradeAgreementAttestationState {
- version: RHI_AGREEMENT_ATTESTATION_STATE_VERSION,
- state: state.clone(),
- };
- let payload = serde_json::to_vec_pretty(&snapshot)?;
- let temp = temp_state_path(&self.path)?;
- tokio::fs::write(&temp, payload).await?;
- tokio::fs::rename(temp, &self.path).await?;
- Ok(())
- }
-}
-
-fn temp_state_path(path: &Path) -> Result<PathBuf, TradeAgreementAttestationRuntimeError> {
- let parent = path.parent().ok_or_else(|| {
- TradeAgreementAttestationRuntimeError::InvalidStatePath(path.to_path_buf())
- })?;
- let file_name = path
- .file_name()
- .and_then(|value| value.to_str())
- .ok_or_else(|| {
- TradeAgreementAttestationRuntimeError::InvalidStatePath(path.to_path_buf())
- })?;
- Ok(parent.join(format!(".{file_name}.tmp")))
-}
-
-pub async fn handle_trade_mutation_event(
- event: Event,
- runtime: TradeAgreementAttestationRuntime,
- policy: &TradeAgreementAttestationPolicy,
-) -> Result<Option<TradeAgreementAttestationReportV1>, TradeAgreementAttestationError> {
- policy.validate()?;
- let kind = event_kind_u32(&event)?;
- if !is_trade_mutation_event_kind(kind) {
- return Err(TradeAgreementAttestationError::UnsupportedKind);
- }
- let mutation = trade_mutation_from_canonical_content(event.content.as_str())?;
- if mutation.mutation_kind().nostr_kind() != kind {
- return Err(TradeAgreementAttestationError::UnsupportedKind);
- }
- let event_author = PublicKey::from_hex(&event.pubkey.to_hex())
- .map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?;
- if event_author != mutation.author_pubkey {
- return Err(TradeAgreementAttestationError::InvalidEventAuthor);
- }
- let mutation_id = mutation
- .mutation_id
- .ok_or(TradeAgreementAttestationError::MissingMutationId)?;
- if !runtime
- .observe_mutation_event(&event, &mutation, &mutation_id, kind)
- .await?
- {
- return Ok(None);
- }
- let projection = runtime.reduce_trade(&mutation.trade_id).await?;
- let Some(claim_id) = projection
- .active_agreement_claim_ids()
- .iter()
- .find(|claim_id| **claim_id == mutation_id)
- .or_else(|| projection.active_agreement_claim_ids().first())
- .cloned()
- else {
- return Ok(None);
- };
- let report = attest_projection_claim(&projection, &claim_id, policy)?;
- runtime.store_report(report.clone()).await?;
- Ok(Some(report))
}
pub fn attest_projection_claim(
@@ -614,13 +238,6 @@ fn mutation_ids_to_strings(values: &[MutationId]) -> Vec<String> {
values.iter().map(MutationId::to_hex).collect()
}
-fn event_kind_u32(event: &Event) -> Result<u32, TradeAgreementAttestationError> {
- match event.kind {
- Kind::Custom(value) => Ok(u32::from(value)),
- _ => Err(TradeAgreementAttestationError::UnsupportedKind),
- }
-}
-
fn validate_optional_hash32(value: &Option<String>) -> Result<(), TradeAgreementAttestationError> {
if let Some(value) = value {
validate_hash32(value)?;
@@ -647,215 +264,3 @@ fn hash_canonical_value(
hasher.update(canonical.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
-
-fn map_notification_recv_result(
- result: Result<RelayPoolNotification, tokio::sync::broadcast::error::RecvError>,
-) -> Result<RelayPoolNotification, ()> {
- result.map_err(|_| ())
-}
-
-async fn subscribe_io(client: &Client, filter: Filter) -> Result<SubscriptionId> {
- client.subscribe(filter).await.map_err(Into::into)
-}
-
-async fn unsubscribe_io(client: &Client, subscription_id: &SubscriptionId) {
- client.unsubscribe(subscription_id).await;
-}
-
-fn should_delay_before_event_handle() -> bool {
- cfg!(all(debug_assertions, not(test)))
-}
-
-async fn process_event_notification(
- event: Event,
- runtime: TradeAgreementAttestationRuntime,
- policy: TradeAgreementAttestationPolicy,
-) -> Result<()> {
- let created_at = u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX);
- if should_delay_before_event_handle() {
- sleep(Duration::from_millis(200)).await;
- }
- match handle_trade_mutation_event(event, runtime.clone(), &policy).await {
- Ok(_) | Err(TradeAgreementAttestationError::UnsupportedKind) => {
- runtime.mark_processed_event(created_at).await?;
- Ok(())
- }
- Err(error) => {
- warn!("rhi agreement attestation rejected event: {error}");
- runtime.mark_processed_event(created_at).await?;
- Ok(())
- }
- }
-}
-
-pub async fn subscriber(
- client: Client,
- _keys: Keys,
- runtime: TradeAgreementAttestationRuntime,
- policy: TradeAgreementAttestationPolicy,
- mut stop_rx: watch::Receiver<bool>,
-) -> Result<()> {
- let subscribed_kinds = trade_mutation_subscription_kinds();
- info!(
- "Starting subscriber for release-product trade mutation kinds: {:?}",
- subscribed_kinds
- );
-
- let kinds: Vec<Kind> = subscribed_kinds
- .iter()
- .map(|kind| u16::try_from(*kind).expect("trade mutation kinds fit in nostr custom range"))
- .map(Kind::Custom)
- .collect();
- let filter = runtime.recovery_filter(kinds).await;
-
- if *stop_rx.borrow() {
- return Ok(());
- }
-
- let subscription_id = subscribe_io(&client, filter).await?;
- let sdk_client = client.clone().into_inner();
- let mut notifications = sdk_client.notifications();
- let mut notifications_closed = false;
-
- loop {
- tokio::select! {
- _ = stop_rx.changed() => {
- break;
- }
- msg = async {
- map_notification_recv_result(notifications.recv().await)
- } => {
- let n = match msg {
- Ok(n) => n,
- Err(_) => {
- notifications_closed = true;
- break;
- }
- };
-
- if let RelayPoolNotification::Event { event, .. } = n {
- let event = (*event).clone();
- process_event_notification(event, runtime.clone(), policy.clone()).await?;
- }
- }
- }
- }
-
- unsubscribe_io(&client, &subscription_id).await;
- if notifications_closed {
- return Err(anyhow!(
- "rhi agreement attestation subscriber notifications closed"
- ));
- }
- Ok(())
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct TradeAgreementAttestationSmokeRequest {
- pub protocol_id: String,
- pub operation: TradeAgreementAttestationSmokeOperation,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TradeAgreementAttestationSmokeOperation {
- Health,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct TradeAgreementAttestationSmokeResponse {
- pub ok: bool,
- pub protocol_id: String,
- pub operation: TradeAgreementAttestationSmokeOperation,
- pub worker_name: String,
- pub worker_version: String,
- pub capabilities: Vec<String>,
- pub error: Option<String>,
-}
-
-pub async fn handle_smoke_request_bytes(bytes: &[u8]) -> TradeAgreementAttestationSmokeResponse {
- match serde_json::from_slice::<TradeAgreementAttestationSmokeRequest>(bytes) {
- Ok(request) if request.protocol_id == RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID => {
- TradeAgreementAttestationSmokeResponse {
- ok: true,
- protocol_id: request.protocol_id,
- operation: request.operation,
- worker_name: "rhi".to_owned(),
- worker_version: env!("CARGO_PKG_VERSION").to_owned(),
- capabilities: vec![
- "release_product_trade_mutation_observation".to_owned(),
- "agreement_claim_attestation_report".to_owned(),
- "no_agreement_authority".to_owned(),
- ],
- error: None,
- }
- }
- Ok(request) => TradeAgreementAttestationSmokeResponse {
- ok: false,
- protocol_id: request.protocol_id,
- operation: request.operation,
- worker_name: "rhi".to_owned(),
- worker_version: env!("CARGO_PKG_VERSION").to_owned(),
- capabilities: Vec::new(),
- error: Some("invalid protocol id".to_owned()),
- },
- Err(error) => TradeAgreementAttestationSmokeResponse {
- ok: false,
- protocol_id: RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID.to_owned(),
- operation: TradeAgreementAttestationSmokeOperation::Health,
- worker_name: "rhi".to_owned(),
- worker_version: env!("CARGO_PKG_VERSION").to_owned(),
- capabilities: Vec::new(),
- error: Some(error.to_string()),
- },
- }
-}
-
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-mod tests {
- use super::{
- RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationRuntime,
- TradeAgreementAttestationRuntimeConfig, TradeAgreementAttestationSmokeOperation,
- TradeAgreementAttestationSmokeRequest, handle_smoke_request_bytes,
- };
- #[tokio::test]
- async fn runtime_persists_and_loads_attestation_state() {
- let temp = tempfile::tempdir().expect("tempdir");
- let config = TradeAgreementAttestationRuntimeConfig {
- state_path: temp.path().join("state.json"),
- replay_window_secs: 100,
- replay_overlap_secs: 10,
- };
- let runtime = TradeAgreementAttestationRuntime::load(config.clone())
- .await
- .expect("load");
- {
- let state = runtime.state();
- state.lock().await.observe_event_created_at(42);
- }
- runtime.persist().await.expect("persist");
- let loaded = TradeAgreementAttestationRuntime::load(config)
- .await
- .expect("load persisted");
- assert_eq!(loaded.state().lock().await.replay_since(100, 100, 10), 32);
- }
-
- #[tokio::test]
- async fn smoke_request_reports_optional_capabilities() {
- let request = TradeAgreementAttestationSmokeRequest {
- protocol_id: RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID.to_owned(),
- operation: TradeAgreementAttestationSmokeOperation::Health,
- };
- let response =
- handle_smoke_request_bytes(&serde_json::to_vec(&request).expect("json")).await;
- assert!(response.ok);
- assert!(
- response
- .capabilities
- .contains(&"no_agreement_authority".to_owned())
- );
- }
-}
diff --git a/src/host_nostr.rs b/src/host_nostr.rs
@@ -1,53 +0,0 @@
-//! RHI-owned relay client over the final portable Nostr contract.
-
-use core::time::Duration;
-
-pub use nostr::{Event, Filter, Keys, Kind, Metadata, SubscriptionId, Timestamp};
-pub use nostr::{Tag, TagKind};
-pub use nostr_sdk::RelayPoolNotification;
-pub use radroots_nostr::event::{ApplicationHandlerSpec, GenericBuilder, ProfileBuilder};
-
-#[derive(Clone)]
-pub struct Client {
- inner: nostr_sdk::Client,
- keys: Keys,
-}
-
-impl Client {
- pub fn new(keys: Keys) -> Self {
- let inner = nostr_sdk::Client::new(keys.clone());
- inner.automatic_authentication(false);
- Self { inner, keys }
- }
-
- pub fn into_inner(self) -> nostr_sdk::Client {
- self.inner
- }
- pub fn keys(&self) -> &Keys {
- &self.keys
- }
- pub async fn connect(&self) {
- self.inner.connect().await;
- }
- pub async fn wait_for_connection(&self, timeout: Duration) {
- self.inner.wait_for_connection(timeout).await;
- }
- pub async fn add_relay(&self, url: &str) -> Result<bool, nostr_sdk::client::Error> {
- self.inner.add_relay(url).await
- }
- pub async fn subscribe(
- &self,
- filter: Filter,
- ) -> Result<SubscriptionId, nostr_sdk::client::Error> {
- Ok(self.inner.subscribe(filter, None).await?.val)
- }
- pub async fn unsubscribe(&self, id: &SubscriptionId) {
- self.inner.unsubscribe(id).await;
- }
- pub async fn send_event(
- &self,
- event: &Event,
- ) -> Result<nostr_sdk::prelude::Output<nostr::EventId>, nostr_sdk::client::Error> {
- self.inner.send_event(event).await
- }
-}
diff --git a/src/host_runtime.rs b/src/host_runtime.rs
@@ -1,119 +0,0 @@
-//! RHI-owned process lifecycle and retry policy.
-
-use core::future::Future;
-use core::time::Duration;
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct NostrServiceConfig {
- pub logs_dir: String,
- #[serde(default)]
- pub relays: Vec<String>,
- #[serde(default)]
- pub nip89_identifier: Option<String>,
- #[serde(default)]
- pub nip89_extra_tags: Vec<Vec<String>>,
-}
-
-const fn default_base_ms() -> u64 {
- 500
-}
-const fn default_max_ms() -> u64 {
- 30_000
-}
-const fn default_factor() -> u32 {
- 2
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-pub struct BackoffConfig {
- #[serde(default = "default_base_ms")]
- pub base_ms: u64,
- #[serde(default = "default_max_ms")]
- pub max_ms: u64,
- #[serde(default = "default_factor")]
- pub factor: u32,
- #[serde(default)]
- pub jitter_ms: u64,
-}
-
-impl Default for BackoffConfig {
- fn default() -> Self {
- Self {
- base_ms: default_base_ms(),
- max_ms: default_max_ms(),
- factor: default_factor(),
- jitter_ms: 0,
- }
- }
-}
-
-impl BackoffConfig {
- fn delay_for_attempt(&self, attempt: u32) -> Duration {
- let base = self.base_ms.max(1);
- let max = self.max_ms.max(base);
- let factor = u64::from(self.factor.max(1));
- let mut delay = base;
- for _ in 0..attempt.saturating_sub(1).min(10) {
- delay = delay.saturating_mul(factor).min(max);
- }
- if self.jitter_ms > 0 {
- let nanos = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap_or_default()
- .subsec_nanos();
- delay = delay
- .saturating_add(u64::from(nanos) % (self.jitter_ms + 1))
- .min(max);
- }
- Duration::from_millis(delay)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Backoff {
- config: BackoffConfig,
- attempt: u32,
-}
-
-impl Backoff {
- pub fn new(config: BackoffConfig) -> Self {
- Self { config, attempt: 0 }
- }
- pub fn reset(&mut self) {
- self.attempt = 0;
- }
- pub fn next_delay(&mut self) -> Duration {
- self.attempt = self.attempt.saturating_add(1);
- self.config.delay_for_attempt(self.attempt)
- }
-}
-
-pub async fn shutdown_signal() {
- let ctrl_c = async {
- tokio::signal::ctrl_c()
- .await
- .expect("install Ctrl+C handler")
- };
- #[cfg(unix)]
- let terminate = async {
- tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
- .expect("install termination handler")
- .recv()
- .await;
- };
- #[cfg(not(unix))]
- let terminate = core::future::pending::<()>();
- wait_for_shutdown(ctrl_c, terminate).await;
-}
-
-async fn wait_for_shutdown<C, T>(ctrl_c: C, terminate: T)
-where
- C: Future<Output = ()>,
- T: Future<Output = ()>,
-{
- tokio::select! { _ = ctrl_c => {}, _ = terminate => {} }
-}
diff --git a/src/lib.rs b/src/lib.rs
@@ -2,14 +2,10 @@
pub mod adapters;
mod cli_v1;
-pub mod config;
mod config_v1;
pub mod features;
pub mod host_identity;
-pub mod host_nostr;
-pub mod host_runtime;
pub mod identity_storage;
-pub mod rhi;
mod runtime_context;
pub use cli_v1::{
@@ -33,480 +29,3 @@ pub use runtime_context::{
RhiRuntimeContext, RhiRuntimeContextError, RhiRuntimeContextErrorKind,
resolve_rhi_runtime_context,
};
-
-use anyhow::{Context, Result, anyhow, bail};
-use radroots_event::{
- envelope::kind::TRADE_MUTATION_EVENT_KINDS,
- profile::{AuthoredProfile, Nip05Identifier},
-};
-use std::time::Duration;
-
-use crate::features::trade_agreement_attestation::{
- TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig,
- trade_mutation_subscription_kinds,
-};
-use crate::host_nostr::{ApplicationHandlerSpec, Metadata, ProfileBuilder};
-use crate::identity_storage::load_service_identity;
-use crate::rhi::{Rhi, start_subscriber_with_policy};
-use radroots_nostr::event::{build_application_handler, build_profile};
-use tracing::{info, warn};
-
-#[cfg(test)]
-static RUN_RHI_AUTO_STOP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
-#[cfg(test)]
-static RUN_RHI_SKIP_SUBSCRIBER: std::sync::atomic::AtomicBool =
- std::sync::atomic::AtomicBool::new(false);
-
-#[cfg(test)]
-static RUN_RHI_BOOTSTRAP_HOOK: std::sync::OnceLock<std::sync::Mutex<Option<Result<(), String>>>> =
- std::sync::OnceLock::new();
-
-#[derive(Clone, Copy)]
-enum RunRhiWaitOutcome {
- Shutdown,
- Stopped,
-}
-
-#[cfg(test)]
-static RUN_RHI_WAIT_HOOK: std::sync::OnceLock<std::sync::Mutex<Option<RunRhiWaitOutcome>>> =
- std::sync::OnceLock::new();
-
-#[cfg(test)]
-fn run_rhi_bootstrap_hook() -> &'static std::sync::Mutex<Option<Result<(), String>>> {
- RUN_RHI_BOOTSTRAP_HOOK.get_or_init(|| std::sync::Mutex::new(None))
-}
-
-#[cfg(test)]
-fn run_rhi_wait_hook() -> &'static std::sync::Mutex<Option<RunRhiWaitOutcome>> {
- RUN_RHI_WAIT_HOOK.get_or_init(|| std::sync::Mutex::new(None))
-}
-
-#[cfg(test)]
-fn take_bootstrap_hook_result() -> Option<Result<(), String>> {
- run_rhi_bootstrap_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .take()
-}
-
-#[cfg(not(test))]
-#[cfg_attr(coverage_nightly, coverage(off))]
-fn take_bootstrap_hook_result() -> Option<Result<(), String>> {
- None
-}
-
-async fn bootstrap_presence(
- client: &crate::host_nostr::Client,
- metadata: &Metadata,
- handler_spec: &ApplicationHandlerSpec,
-) -> Result<()> {
- if let Some(result) = take_bootstrap_hook_result() {
- return result.map_err(anyhow::Error::msg);
- }
-
- client.connect().await;
- client.wait_for_connection(Duration::from_secs(5)).await;
-
- let profile_event = build_authored_service_profile_event(metadata)?
- .sign_with_keys(client.keys())
- .context("sign strict RHI service Profile")?;
- client
- .send_event(&profile_event)
- .await
- .context("publish strict RHI service Profile")?;
-
- let handler_event = build_application_handler(handler_spec)
- .context("build RHI application-handler event")?
- .sign_with_keys(client.keys())
- .context("sign RHI application-handler event")?;
- client
- .send_event(&handler_event)
- .await
- .context("publish RHI application-handler event")?;
- Ok(())
-}
-
-fn build_authored_service_profile_event(metadata: &Metadata) -> Result<ProfileBuilder> {
- let profile = authored_service_profile(metadata)?;
- build_profile(&profile).context("build strict RHI service Profile event")
-}
-
-fn authored_service_profile(metadata: &Metadata) -> Result<AuthoredProfile> {
- if metadata.picture.is_some() || metadata.banner.is_some() {
- bail!(
- "RHI service Profile media requires byte-verified Blossom descriptors and proven BUD-02 upload completion"
- );
- }
- if metadata.website.is_some() || metadata.lud06.is_some() || metadata.lud16.is_some() {
- bail!("RHI service Profile contains fields outside the strict authored contract");
- }
-
- let name = metadata
- .name
- .clone()
- .ok_or_else(|| anyhow!("RHI service Profile requires metadata.name"))?;
- let mut profile =
- AuthoredProfile::new(name).context("validate strict RHI service Profile name")?;
- if let Some(display_name) = metadata.display_name.as_ref() {
- profile = profile.with_display_name(display_name.clone());
- }
- if let Some(about) = metadata.about.as_ref() {
- profile = profile.with_about(about.clone());
- }
- if let Some(nip05) = metadata.nip05.as_deref() {
- profile = profile.with_nip05(
- Nip05Identifier::parse(nip05)
- .context("validate strict RHI service Profile NIP-05 identifier")?,
- );
- }
-
- for key in metadata.custom.keys() {
- if key != "bot" {
- bail!("RHI service Profile contains unsupported metadata field `{key}`");
- }
- }
- if let Some(bot) = metadata.custom.get("bot") {
- profile = profile.with_bot(
- bot.as_bool()
- .ok_or_else(|| anyhow!("RHI service Profile `bot` field must be a Boolean"))?,
- );
- }
-
- Ok(profile)
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-async fn wait_for_shutdown_or_stopped(handle: crate::rhi::RhiHandle) -> RunRhiWaitOutcome {
- #[cfg(test)]
- if let Some(outcome) = run_rhi_wait_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .take()
- {
- return outcome;
- }
-
- tokio::select! {
- _ = crate::host_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown,
- _ = handle.stopped() => RunRhiWaitOutcome::Stopped,
- }
-}
-
-pub async fn run_rhi(settings: &config::Settings, context: &RhiRuntimeContext) -> Result<()> {
- let identity = load_service_identity(context.identity_path())?;
- let keys = identity.keys().clone();
- let agreement_attestation_runtime =
- TradeAgreementAttestationRuntime::load(TradeAgreementAttestationRuntimeConfig {
- state_path: settings.config.subscriber.state.path.clone(),
- replay_window_secs: settings.config.subscriber.state.replay_window_secs,
- replay_overlap_secs: settings.config.subscriber.state.replay_overlap_secs,
- })
- .await?;
-
- let rhi = Rhi::with_agreement_attestation_runtime_and_policy(
- keys.clone(),
- agreement_attestation_runtime,
- settings.config.trade_agreement_attestation.clone(),
- );
- let client = rhi.client.clone();
- let service_cfg = settings.config.service.clone();
- let relays = service_cfg.relays.clone();
-
- for relay in &relays {
- client.add_relay(relay).await?;
- }
-
- let md = settings.metadata.clone();
-
- if !relays.is_empty() {
- let handler_kinds = trade_mutation_subscription_kinds();
- let mut handler_spec = ApplicationHandlerSpec::new(handler_kinds)
- .with_metadata(md.clone())
- .with_extra_tags(service_cfg.nip89_extra_tags.clone())
- .with_relays(relays.clone());
- if let Some(identifier) = service_cfg.nip89_identifier.clone() {
- handler_spec = handler_spec.with_identifier(identifier);
- }
- if let Err(e) = bootstrap_presence(&client, &md, &handler_spec).await {
- warn!("Failed to publish service presence on startup: {e}");
- } else {
- info!("Published service presence on startup");
- }
- }
-
- #[cfg(test)]
- if RUN_RHI_SKIP_SUBSCRIBER.load(std::sync::atomic::Ordering::Relaxed) {
- return Ok(());
- }
-
- let handle = start_subscriber_with_policy(
- client.clone(),
- keys.clone(),
- rhi.agreement_attestation_runtime.clone(),
- rhi.agreement_attestation_policy.clone(),
- settings.config.subscriber.backoff.clone(),
- )
- .await;
-
- let stop_handle = handle.clone();
-
- #[cfg(test)]
- if RUN_RHI_AUTO_STOP.load(std::sync::atomic::Ordering::Relaxed) {
- stop_handle.stop();
- }
-
- match wait_for_shutdown_or_stopped(handle).await {
- RunRhiWaitOutcome::Shutdown => {
- info!("Shutting down");
- stop_handle.stop();
- }
- RunRhiWaitOutcome::Stopped => {}
- }
-
- let sdk_client = client.into_inner();
- sdk_client.unsubscribe_all().await;
- sdk_client.disconnect().await;
-
- Ok(())
-}
-
-pub fn release_product_handler_kinds() -> &'static [u32] {
- &TRADE_MUTATION_EVENT_KINDS
-}
-
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-mod tests {
- use super::{
- RUN_RHI_AUTO_STOP, RUN_RHI_SKIP_SUBSCRIBER, RunRhiWaitOutcome, authored_service_profile,
- bootstrap_presence, build_authored_service_profile_event, release_product_handler_kinds,
- run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook,
- };
- use crate::{config, parse_rhi_cli_v1_from, resolve_rhi_runtime_context};
- use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS;
- use std::sync::atomic::Ordering;
- use tokio::sync::{Mutex, MutexGuard};
-
- static TEST_LOCK: Mutex<()> = Mutex::const_new(());
-
- async fn test_guard() -> MutexGuard<'static, ()> {
- let guard = TEST_LOCK.lock().await;
- RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed);
- RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed);
- *run_rhi_bootstrap_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
- *run_rhi_wait_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
- guard
- }
-
- fn settings_with_relays(
- relays: Vec<String>,
- context: &crate::RhiRuntimeContext,
- ) -> config::Settings {
- config::Settings {
- metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"),
- config: config::Configuration {
- service: crate::host_runtime::NostrServiceConfig {
- logs_dir: std::env::temp_dir()
- .join("rhi-test-logs")
- .display()
- .to_string(),
- relays,
- nip89_identifier: Some("rhi".to_string()),
- nip89_extra_tags: Vec::new(),
- },
- logging: config::LoggingConfig {
- output_dir: std::env::temp_dir().join("rhi-test-logs"),
- filter: "info".to_string(),
- stdout: true,
- },
- subscriber: config::SubscriberConfig {
- backoff: crate::host_runtime::BackoffConfig {
- base_ms: 1,
- max_ms: 2,
- factor: 1,
- jitter_ms: 0,
- },
- state: config::SubscriberStateConfig {
- path: context
- .context()
- .paths()
- .state()
- .join("trade-agreement-attestation/state.json"),
- replay_window_secs: 24 * 60 * 60,
- replay_overlap_secs: 5 * 60,
- },
- },
- trade_agreement_attestation:
- crate::features::trade_agreement_attestation::TradeAgreementAttestationPolicy::default(),
- },
- }
- }
-
- fn context_for_root(root: &std::path::Path) -> crate::RhiRuntimeContext {
- let root = root.to_str().expect("UTF-8 temp root");
- let invocation = parse_rhi_cli_v1_from([
- "rhi",
- "--profile",
- "repo-local",
- "--instance",
- "default",
- "--repo-local-root",
- root,
- "run",
- ])
- .expect("invocation");
- resolve_rhi_runtime_context(
- &crate::RadrootsPathResolver::new(
- crate::RadrootsPlatform::Linux,
- crate::RadrootsHostEnvironment::default(),
- ),
- &invocation,
- )
- .expect("context")
- }
-
- fn provision_identity(context: &crate::RhiRuntimeContext) {
- crate::identity_storage::store_encrypted_identity(
- context.identity_path(),
- &crate::host_identity::RadrootsIdentity::generate(),
- )
- .expect("identity");
- }
-
- #[tokio::test]
- async fn run_rhi_starts_and_stops_without_relays() {
- let _guard = test_guard().await;
- RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed);
- let temp = tempfile::tempdir().expect("tempdir");
- let context = context_for_root(temp.path());
- provision_identity(&context);
- let settings = settings_with_relays(Vec::new(), &context);
- run_rhi(&settings, &context).await.expect("run rhi");
- }
-
- #[tokio::test]
- async fn run_rhi_bootstraps_release_product_handler_kinds_when_relays_exist() {
- let _guard = test_guard().await;
- RUN_RHI_SKIP_SUBSCRIBER.store(true, Ordering::Relaxed);
- *run_rhi_bootstrap_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Ok(()));
- let temp = tempfile::tempdir().expect("tempdir");
- let context = context_for_root(temp.path());
- provision_identity(&context);
- let settings = settings_with_relays(vec!["wss://relay.example.com".to_string()], &context);
- run_rhi(&settings, &context).await.expect("run rhi");
- assert_eq!(release_product_handler_kinds(), TRADE_MUTATION_EVENT_KINDS);
- }
-
- #[tokio::test]
- async fn run_rhi_stops_on_wait_hook() {
- let _guard = test_guard().await;
- *run_rhi_wait_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped);
- let temp = tempfile::tempdir().expect("tempdir");
- let context = context_for_root(temp.path());
- provision_identity(&context);
- let settings = settings_with_relays(Vec::new(), &context);
- run_rhi(&settings, &context).await.expect("run rhi");
- }
-
- #[tokio::test]
- async fn bootstrap_presence_reports_hook_error() {
- let _guard = test_guard().await;
- *run_rhi_bootstrap_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner) =
- Some(Err("forced bootstrap failure".to_string()));
- let keys = crate::host_nostr::Keys::generate();
- let client = crate::host_nostr::Client::new(keys.clone());
- let metadata: crate::host_nostr::Metadata =
- serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata");
- let spec =
- crate::host_nostr::ApplicationHandlerSpec::new(TRADE_MUTATION_EVENT_KINDS.to_vec())
- .with_identifier("rhi")
- .with_metadata(metadata.clone());
- let err = bootstrap_presence(&client, &metadata, &spec)
- .await
- .expect_err("forced error");
- assert!(format!("{err:#}").contains("forced bootstrap failure"));
- }
-
- #[test]
- fn service_profile_uses_only_strict_authored_fields() {
- let metadata: crate::host_nostr::Metadata = serde_json::from_str(
- r#"{
- "name":"rhi",
- "display_name":"Radroots agreement attestation",
- "about":"Attests trade agreement projections",
- "nip05":"rhi@EXAMPLE.COM",
- "bot":true
- }"#,
- )
- .expect("metadata");
-
- let profile = authored_service_profile(&metadata).expect("strict profile");
-
- assert_eq!(profile.name(), "rhi");
- assert_eq!(
- profile.display_name(),
- Some("Radroots agreement attestation")
- );
- assert_eq!(profile.about(), Some("Attests trade agreement projections"));
- assert_eq!(
- profile.nip05().map(|identifier| identifier.as_str()),
- Some("rhi@example.com")
- );
- assert_eq!(profile.bot(), Some(true));
- assert!(profile.picture().is_none());
- assert!(profile.banner().is_none());
-
- let keys = crate::host_nostr::Keys::generate();
- let event = build_authored_service_profile_event(&metadata)
- .expect("strict Profile event")
- .sign_with_keys(&keys)
- .expect("sign strict Profile event");
- assert_eq!(event.kind.as_u16(), 0);
- assert!(event.tags.is_empty());
- assert_eq!(
- event.content,
- r#"{"name":"rhi","display_name":"Radroots agreement attestation","about":"Attests trade agreement projections","nip05":"rhi@example.com","bot":true}"#
- );
- }
-
- #[test]
- fn service_profile_rejects_unverified_media_and_unsupported_fields() {
- for (metadata, expected) in [
- (
- r#"{"name":"rhi","picture":"https://cdn.example/rhi.png"}"#,
- "byte-verified Blossom descriptors",
- ),
- (
- r#"{"name":"rhi","website":"https://radroots.org"}"#,
- "outside the strict authored contract",
- ),
- (
- r#"{"name":"rhi","bot":"yes"}"#,
- "`bot` field must be a Boolean",
- ),
- (
- r#"{"name":"rhi","legacy_role":"worker"}"#,
- "unsupported metadata field `legacy_role`",
- ),
- (r#"{}"#, "requires metadata.name"),
- (
- r#"{"name":"rhi","nip05":"RHI@example.com"}"#,
- "validate strict RHI service Profile NIP-05 identifier",
- ),
- ] {
- let metadata = serde_json::from_str(metadata).expect("metadata");
- let error = authored_service_profile(&metadata).expect_err("must fail closed");
- assert!(format!("{error:#}").contains(expected), "{error:#}");
- }
- }
-}
diff --git a/src/main.rs b/src/main.rs
@@ -3,10 +3,9 @@
use std::path::PathBuf;
use std::process::ExitCode;
-use anyhow::{Context, Result, bail};
use rhi::{
- RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiCommandV1,
- RhiRuntimeContext, parse_rhi_cli_v1_from, resolve_rhi_runtime_context, run_rhi,
+ RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, parse_rhi_cli_v1_from,
+ resolve_rhi_runtime_context,
};
fn main() -> ExitCode {
@@ -14,17 +13,10 @@ fn main() -> ExitCode {
Ok(invocation) => invocation,
Err(_) => return ExitCode::FAILURE,
};
- let runtime = match tokio::runtime::Builder::new_multi_thread()
- .enable_all()
- .build()
- {
- Ok(runtime) => runtime,
- Err(_) => return ExitCode::FAILURE,
- };
- exit_code_from_run(runtime.block_on(execute(invocation)))
+ exit_code_from_run(execute(invocation))
}
-fn exit_code_from_run(result: Result<()>) -> ExitCode {
+fn exit_code_from_run(result: Result<(), ()>) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(_) => {
@@ -34,49 +26,10 @@ fn exit_code_from_run(result: Result<()>) -> ExitCode {
}
}
-async fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<()> {
+fn execute(invocation: rhi::RhiCliInvocationV1) -> Result<(), ()> {
let resolver = RadrootsPathResolver::new(RadrootsPlatform::current(), host_environment());
- let context = resolve_rhi_runtime_context(&resolver, &invocation)
- .map_err(|_| anyhow::anyhow!("RHI runtime context is invalid"))?;
- match invocation.command() {
- RhiCommandV1::Run => execute_run(&context).await,
- _ => bail!("RHI command execution is not available in this checkpoint"),
- }
-}
-
-async fn execute_run(context: &RhiRuntimeContext) -> Result<()> {
- let settings = rhi::config::load_settings_from_path(context.selected_config_path(), context)
- .context("load RHI configuration")?;
- init_rhi_logging(&settings)?;
- run_rhi(&settings, context).await
-}
-
-fn init_rhi_logging(settings: &rhi::config::Settings) -> Result<()> {
- use tracing_subscriber::fmt::writer::MakeWriterExt as _;
-
- std::fs::create_dir_all(&settings.config.logging.output_dir)
- .context("create RHI log directory")?;
- let appender = tracing_appender::rolling::daily(&settings.config.logging.output_dir, "rhi.log");
- let (writer, guard) = tracing_appender::non_blocking(appender);
- static LOG_GUARD: std::sync::OnceLock<tracing_appender::non_blocking::WorkerGuard> =
- std::sync::OnceLock::new();
- let filter = tracing_subscriber::EnvFilter::new(&settings.config.logging.filter);
- if settings.config.logging.stdout {
- tracing_subscriber::fmt()
- .with_env_filter(filter)
- .with_writer(writer.and(std::io::stdout))
- .try_init()
- .map_err(|_| anyhow::anyhow!("initialize RHI logging"))?;
- } else {
- tracing_subscriber::fmt()
- .with_env_filter(filter)
- .with_writer(writer)
- .try_init()
- .map_err(|_| anyhow::anyhow!("initialize RHI logging"))?;
- }
- LOG_GUARD
- .set(guard)
- .map_err(|_| anyhow::anyhow!("RHI logging is already initialized"))
+ let _context = resolve_rhi_runtime_context(&resolver, &invocation).map_err(|_| ())?;
+ Err(())
}
fn host_environment() -> RadrootsHostEnvironment {
@@ -99,15 +52,35 @@ fn host_environment() -> RadrootsHostEnvironment {
#[cfg(test)]
mod tests {
- use super::exit_code_from_run;
+ use super::{execute, exit_code_from_run};
+ use rhi::parse_rhi_cli_v1_from;
use std::process::ExitCode;
#[test]
fn process_result_is_stable() {
assert_eq!(exit_code_from_run(Ok(())), ExitCode::SUCCESS);
+ assert_eq!(exit_code_from_run(Err(())), ExitCode::FAILURE);
+ }
+
+ #[test]
+ fn admitted_command_fails_closed_without_creating_runtime_state() {
+ let root = tempfile::tempdir().expect("temporary repo-local root");
+ let invocation = parse_rhi_cli_v1_from([
+ "rhi",
+ "--profile",
+ "repo-local",
+ "--instance",
+ "default",
+ "--repo-local-root",
+ root.path().to_str().expect("UTF-8 test root"),
+ "run",
+ ])
+ .expect("valid invocation");
+
+ assert_eq!(execute(invocation), Err(()));
assert_eq!(
- exit_code_from_run(Err(anyhow::anyhow!("secret path"))),
- ExitCode::FAILURE
+ std::fs::read_dir(root.path()).expect("read root").count(),
+ 0
);
}
}
diff --git a/src/rhi.rs b/src/rhi.rs
@@ -1,345 +0,0 @@
-#![cfg_attr(coverage_nightly, coverage(off))]
-
-use std::sync::Arc;
-use std::time::{Duration, Instant};
-
-use crate::host_nostr::{Client, Keys};
-use crate::host_runtime::{Backoff, BackoffConfig};
-use tokio::sync::Mutex;
-
-use crate::features::trade_agreement_attestation::{
- TradeAgreementAttestationPolicy, TradeAgreementAttestationRuntime,
-};
-
-#[cfg(not(test))]
-fn connection_wait_timeout() -> Duration {
- Duration::from_secs(5)
-}
-
-#[cfg(test)]
-fn connection_wait_timeout() -> Duration {
- Duration::from_millis(10)
-}
-
-#[cfg(test)]
-static SUBSCRIBER_RESULT_HOOK: std::sync::OnceLock<
- std::sync::Mutex<std::collections::VecDeque<Result<(), anyhow::Error>>>,
-> = std::sync::OnceLock::new();
-
-#[cfg(test)]
-fn subscriber_result_hook()
--> &'static std::sync::Mutex<std::collections::VecDeque<Result<(), anyhow::Error>>> {
- SUBSCRIBER_RESULT_HOOK.get_or_init(|| std::sync::Mutex::new(std::collections::VecDeque::new()))
-}
-
-async fn run_subscriber_once(
- client: Client,
- keys: Keys,
- runtime: TradeAgreementAttestationRuntime,
- policy: TradeAgreementAttestationPolicy,
- stop_rx: tokio::sync::watch::Receiver<bool>,
-) -> Result<(), anyhow::Error> {
- #[cfg(test)]
- if let Some(result) = subscriber_result_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .pop_front()
- {
- return result;
- }
-
- crate::features::trade_agreement_attestation::subscriber(client, keys, runtime, policy, stop_rx)
- .await
-}
-
-async fn wait_for_connection_or_stop(
- client: &Client,
- stop_rx: &mut tokio::sync::watch::Receiver<bool>,
-) -> bool {
- if *stop_rx.borrow() {
- return false;
- }
- tokio::select! {
- _ = client.wait_for_connection(connection_wait_timeout()) => true,
- _ = stop_rx.changed() => false,
- }
-}
-
-pub struct Rhi {
- pub(crate) _started: Instant,
- pub client: Client,
- pub(crate) agreement_attestation_runtime: TradeAgreementAttestationRuntime,
- pub(crate) agreement_attestation_policy: TradeAgreementAttestationPolicy,
-}
-
-impl Rhi {
- pub fn new(keys: Keys) -> Self {
- Self::with_agreement_attestation_runtime(keys, TradeAgreementAttestationRuntime::new())
- }
-
- pub fn with_agreement_attestation_runtime(
- keys: Keys,
- agreement_attestation_runtime: TradeAgreementAttestationRuntime,
- ) -> Self {
- Self::with_agreement_attestation_runtime_and_policy(
- keys,
- agreement_attestation_runtime,
- TradeAgreementAttestationPolicy::default(),
- )
- }
-
- pub fn with_agreement_attestation_runtime_and_policy(
- keys: Keys,
- agreement_attestation_runtime: TradeAgreementAttestationRuntime,
- agreement_attestation_policy: TradeAgreementAttestationPolicy,
- ) -> Self {
- let client = Client::new(keys);
- Self {
- _started: Instant::now(),
- client,
- agreement_attestation_runtime,
- agreement_attestation_policy,
- }
- }
-}
-
-pub struct RhiHandle {
- stop_tx: Arc<Mutex<Option<tokio::sync::watch::Sender<bool>>>>,
- join: Option<tokio::task::JoinHandle<()>>,
-}
-
-impl Clone for RhiHandle {
- fn clone(&self) -> Self {
- Self {
- stop_tx: Arc::clone(&self.stop_tx),
- join: None,
- }
- }
-}
-
-impl RhiHandle {
- pub fn stop(&self) {
- if let Some(tx) = self.stop_tx.try_lock().ok().and_then(|mut opt| opt.take()) {
- let _ = tx.send(true);
- }
- }
-
- pub async fn stopped(mut self) {
- if let Some(join) = self.join.take() {
- let _ = join.await;
- }
- }
-}
-
-pub async fn start_subscriber(
- client: Client,
- keys: Keys,
- runtime: TradeAgreementAttestationRuntime,
- backoff_cfg: BackoffConfig,
-) -> RhiHandle {
- start_subscriber_with_policy(
- client,
- keys,
- runtime,
- TradeAgreementAttestationPolicy::default(),
- backoff_cfg,
- )
- .await
-}
-
-pub async fn start_subscriber_with_policy(
- client: Client,
- keys: Keys,
- runtime: TradeAgreementAttestationRuntime,
- policy: TradeAgreementAttestationPolicy,
- backoff_cfg: BackoffConfig,
-) -> RhiHandle {
- let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false);
-
- let join = tokio::spawn(async move {
- let mut backoff = Backoff::new(backoff_cfg);
- loop {
- if *stop_rx.borrow() {
- break;
- }
-
- client.connect().await;
- if !wait_for_connection_or_stop(&client, &mut stop_rx).await {
- break;
- }
-
- let res = run_subscriber_once(
- client.clone(),
- keys.clone(),
- runtime.clone(),
- policy.clone(),
- stop_rx.clone(),
- )
- .await;
-
- let failed = res.is_err();
-
- if let Err(e) = res {
- tracing::error!("Error on agreement attestation subscription: {e}");
- } else {
- backoff.reset();
- }
-
- if *stop_rx.borrow() {
- break;
- }
-
- if failed {
- let delay = backoff.next_delay();
- tokio::select! {
- _ = tokio::time::sleep(delay) => {}
- _ = stop_rx.changed() => break,
- }
- }
- }
- });
-
- RhiHandle {
- stop_tx: Arc::new(Mutex::new(Some(stop_tx))),
- join: Some(join),
- }
-}
-
-#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
-mod tests {
- use super::{
- Rhi, RhiHandle, start_subscriber, subscriber_result_hook, wait_for_connection_or_stop,
- };
- use crate::features::trade_agreement_attestation::TradeAgreementAttestationRuntime;
- use crate::host_nostr::{Client, Keys};
- use crate::host_runtime::BackoffConfig;
- use anyhow::anyhow;
- use std::sync::Arc;
- use tokio::sync::Mutex;
-
- #[tokio::test]
- async fn rhi_new_initializes_client_and_runtime() {
- let keys = Keys::generate();
- let rhi = Rhi::new(keys);
- let _ = rhi.client.clone();
- assert!(rhi.agreement_attestation_runtime.reports().await.is_empty());
- }
-
- #[tokio::test]
- async fn rhi_handle_stop_and_stopped_cover_paths() {
- let (tx, _rx) = tokio::sync::watch::channel(false);
- let join = tokio::spawn(async {});
- let handle = RhiHandle {
- stop_tx: Arc::new(Mutex::new(Some(tx))),
- join: Some(join),
- };
- handle.stop();
- handle.stop();
- handle.clone().stopped().await;
- handle.stopped().await;
- }
-
- #[tokio::test]
- async fn start_subscriber_runs_with_and_without_relay() {
- let keys = Keys::generate();
- let cfg = BackoffConfig {
- base_ms: 1,
- max_ms: 2,
- factor: 1,
- jitter_ms: 0,
- };
-
- let client_err = Client::new(keys.clone());
- let handle_err = start_subscriber(
- client_err,
- keys.clone(),
- TradeAgreementAttestationRuntime::new(),
- cfg.clone(),
- )
- .await;
- tokio::time::sleep(std::time::Duration::from_millis(30)).await;
- handle_err.stop();
- handle_err.stopped().await;
-
- let client_ok = Client::new(keys.clone());
- let _ = client_ok.add_relay("wss://relay.example.com").await;
- subscriber_result_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .push_back(Ok(()));
- let handle_ok = start_subscriber(
- client_ok,
- keys,
- TradeAgreementAttestationRuntime::new(),
- cfg,
- )
- .await;
- tokio::time::sleep(std::time::Duration::from_millis(30)).await;
- handle_ok.stop();
- handle_ok.stopped().await;
- }
-
- #[tokio::test]
- async fn start_subscriber_stops_during_connection_wait_branch() {
- let keys = Keys::generate();
- let client = Client::new(keys.clone());
- let handle = start_subscriber(
- client,
- keys,
- TradeAgreementAttestationRuntime::new(),
- BackoffConfig {
- base_ms: 25,
- max_ms: 50,
- factor: 1,
- jitter_ms: 0,
- },
- )
- .await;
- tokio::time::sleep(std::time::Duration::from_millis(5)).await;
- handle.stop();
- handle.stopped().await;
- }
-
- #[tokio::test]
- async fn start_subscriber_stops_during_backoff_wait_branch() {
- let keys = Keys::generate();
- let client = Client::new(keys.clone());
- let _ = client.add_relay("wss://relay.example.com").await;
- subscriber_result_hook()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner)
- .push_back(Err(anyhow!("forced subscriber failure")));
- let handle = start_subscriber(
- client,
- keys,
- TradeAgreementAttestationRuntime::new(),
- BackoffConfig {
- base_ms: 200,
- max_ms: 200,
- factor: 1,
- jitter_ms: 0,
- },
- )
- .await;
- tokio::time::sleep(std::time::Duration::from_millis(25)).await;
- handle.stop();
- handle.stopped().await;
- }
-
- #[tokio::test]
- async fn wait_for_connection_or_stop_covers_both_outcomes() {
- let keys = Keys::generate();
-
- let client_stop = Client::new(keys.clone());
- let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false);
- let _ = stop_tx.send(true);
- let stop_branch = wait_for_connection_or_stop(&client_stop, &mut stop_rx).await;
- assert!(!stop_branch);
-
- let client_wait = Client::new(keys);
- let (_tx, mut rx) = tokio::sync::watch::channel(false);
- let wait_branch = wait_for_connection_or_stop(&client_wait, &mut rx).await;
- assert!(wait_branch);
- }
-}
diff --git a/tests/services_hardening_runtime_context.rs b/tests/services_hardening_runtime_context.rs
@@ -263,7 +263,6 @@ fn source_contains_no_legacy_path_authority() {
let lib = include_str!("../src/lib.rs");
let context = include_str!("../src/runtime_context.rs");
let main = include_str!("../src/main.rs");
- let config = include_str!("../src/config.rs");
for forbidden in [
"pub mod host_paths",
"pub mod paths",
@@ -279,9 +278,13 @@ fn source_contains_no_legacy_path_authority() {
assert!(!lib.contains(forbidden));
assert!(!context.contains(forbidden));
assert!(!main.contains(forbidden));
- assert!(!config.contains(forbidden));
}
- for removed in ["src/paths.rs", "src/host_paths/mod.rs", "src/cli.rs"] {
+ for removed in [
+ "src/paths.rs",
+ "src/host_paths/mod.rs",
+ "src/cli.rs",
+ "src/config.rs",
+ ] {
assert!(
!Path::new(env!("CARGO_MANIFEST_DIR")).join(removed).exists(),
"legacy path authority remains at {removed}"
diff --git a/tests/services_hardening_wave_100_a.rs b/tests/services_hardening_wave_100_a.rs
@@ -0,0 +1,84 @@
+#![forbid(unsafe_code)]
+
+use std::path::Path;
+
+use rhi::{RHI_CONFIG_SCHEMA, RhiConfigProfile, parse_rhi_config_v1};
+use serde_json::Value;
+
+const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json");
+const SOURCE_LOCK: &str = include_str!("../radroots.service.source-lock.v2.toml");
+
+#[test]
+fn canonical_example_agrees_with_the_exact_schema_and_parser() {
+ let schema: Value = serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema");
+ let example: toml::Value = toml::from_str(CONFIG_EXAMPLE).expect("canonical example TOML");
+ let example = serde_json::to_value(example).expect("canonical example JSON projection");
+ let errors = jsonschema::validator_for(&schema)
+ .expect("configuration schema compiles")
+ .iter_errors(&example)
+ .map(|error| error.to_string())
+ .collect::<Vec<_>>();
+ assert!(errors.is_empty(), "schema/parser example drift: {errors:?}");
+
+ let document = parse_rhi_config_v1(CONFIG_EXAMPLE.as_bytes(), RhiConfigProfile::Production)
+ .expect("the canonical example must pass the production parser");
+ assert_eq!(document.schema(), RHI_CONFIG_SCHEMA);
+ assert_eq!(document.profile(), RhiConfigProfile::Production);
+ assert!(document.effective().field_count() > 0);
+}
+
+#[test]
+fn wave_one_removed_files_and_predecessor_lock_are_absent() {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
+ for removed in [
+ "config.toml",
+ "flake.lock",
+ "flake.nix",
+ "radroots.lib.source-lock.v1.toml",
+ "src/config.rs",
+ "src/host_nostr.rs",
+ "src/host_runtime.rs",
+ "src/rhi.rs",
+ ] {
+ assert!(
+ !root.join(removed).exists(),
+ "removed path remains: {removed}"
+ );
+ }
+ assert!(root.join("radroots.service.source-lock.v2.toml").is_file());
+ assert!(SOURCE_LOCK.starts_with(
+ "schema = \"radroots.service.source-lock.v2\"\ncontract_version = 2\nservice = \"rhi\"\n"
+ ));
+ for forbidden in [
+ "lib_revision =",
+ "flake_lock_sha256",
+ "material = \"present\"",
+ ] {
+ assert!(
+ !SOURCE_LOCK.contains(forbidden),
+ "source lock retains predecessor behavior: {forbidden}"
+ );
+ }
+}
+
+#[test]
+fn executable_has_no_prototype_runtime_fallback() {
+ let main = include_str!("../src/main.rs");
+ for forbidden in [
+ "load_settings_from_path",
+ "run_rhi",
+ "init_rhi_logging",
+ "tokio::runtime",
+ "tracing_subscriber",
+ "RHI_",
+ ] {
+ assert!(
+ !main.contains(forbidden),
+ "executable retains prototype fallback: {forbidden}"
+ );
+ }
+ assert!(main.contains("parse_rhi_cli_v1_from"));
+ assert!(main.contains("resolve_rhi_runtime_context"));
+ assert!(main.contains("Err(())"));
+}
diff --git a/tests/source_guards.rs b/tests/source_guards.rs
@@ -100,73 +100,40 @@ fn rhi_release_product_surface_has_no_order_or_receipt_modules() {
}
#[test]
-fn rhi_agreement_attestation_is_release_product_optional_infrastructure() {
- let worker = read_repo_file("src/features/trade_agreement_attestation.rs");
- let lib = read_repo_file("src/lib.rs");
+fn rhi_agreement_attestation_retains_only_the_pure_foundation() {
+ let attestation = read_repo_file("src/features/trade_agreement_attestation.rs");
let cli = read_repo_file("src/cli_v1.rs");
- let config = read_repo_file("src/config.rs");
for required in [
"RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID",
"TradeAgreementAttestationPolicy",
"LocalStatementHash",
- "TradeAgreementAttestationRuntime",
- "handle_trade_mutation_event",
"attest_projection_claim",
- "claim_mutation_id",
"projection_digest",
"RadrootsTradeAttestationResultV1::Valid",
"RadrootsTradeAttestationResultV1::Invalid",
"TRADE_MUTATION_EVENT_KINDS",
- "is_trade_mutation_event_kind",
- "no_agreement_authority",
"expected_statement_contract_hash",
] {
assert!(
- worker.contains(required),
- "agreement attestation worker must retain release-product requirement `{required}`"
+ attestation.contains(required),
+ "agreement attestation foundation must retain release-product requirement `{required}`"
);
}
assert!(
- lib.contains("trade_mutation_subscription_kinds()")
- && lib.contains("TRADE_MUTATION_EVENT_KINDS")
- && lib.contains("AuthoredProfile")
- && lib.contains("ProfileBuilder")
- && lib.contains("build_profile")
- && lib.contains("build_application_handler")
- && lib.contains(".send_event(")
- && !lib.contains("radroots_nostr::prelude")
- && lib.contains("client.into_inner()"),
- "RHI service presence must advertise canonical release-product trade mutation kinds"
- );
- assert!(
!cli.contains("AttestationSmoke")
&& !cli.contains("ProofSmoke")
&& !cli.contains("remote-prove"),
"RHI CLI must not retain prototype smoke commands"
);
- assert!(
- config.contains("settings.config.trade_agreement_attestation.validate()?"),
- "RHI config loading must validate the canonical attestation policy"
- );
}
#[test]
-fn rhi_state_paths_are_named_for_agreement_attestation() {
- let config = read_repo_file("src/config.rs");
+fn rhi_runtime_context_retains_only_governed_artifacts() {
let context = read_repo_file("src/runtime_context.rs");
- assert!(
- config.contains("trade-agreement-attestation"),
- "transitional RHI state paths must use agreement-attestation naming"
- );
- for source in [config.as_str(), context.as_str()] {
- assert!(
- !source.contains("trade-listing"),
- "RHI runtime state paths must not retain trade-listing naming"
- );
- }
+ assert!(!context.contains("trade-listing"));
assert!(
context.contains("default_service_instance_artifacts")
&& context.contains("service.identity.ncrypt"),
@@ -174,6 +141,51 @@ fn rhi_state_paths_are_named_for_agreement_attestation() {
);
}
+#[test]
+fn rhi_wave_one_removes_prototype_runtime_and_selection_authority() {
+ for forbidden_path in [
+ "config.toml",
+ "flake.lock",
+ "flake.nix",
+ "radroots.lib.source-lock.v1.toml",
+ "src/config.rs",
+ "src/host_nostr.rs",
+ "src/host_runtime.rs",
+ "src/rhi.rs",
+ ] {
+ assert!(
+ !Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join(forbidden_path)
+ .exists(),
+ "RHI must not retain removed wave-one path `{forbidden_path}`"
+ );
+ }
+
+ for (path, source) in rust_sources_under("src") {
+ for forbidden in [
+ "load_settings_from_path",
+ "TradeAgreementAttestationRuntime",
+ "TradeAgreementAttestationStatePersistence",
+ "TradeAgreementAttestationSmoke",
+ "handle_smoke_request_bytes",
+ "worker_name",
+ "state.json",
+ "std::env::var(\"RHI_",
+ "std::env::var_os(\"RHI_",
+ "worker_root",
+ "nostr_sdk::Client",
+ "tokio::signal",
+ "tracing_appender",
+ "tracing_subscriber",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "{path} retains removed wave-one authority `{forbidden}`"
+ );
+ }
+ }
+}
+
fn read_repo_file(relative_path: &str) -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
fs::read_to_string(path.as_path())