rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit fda4c3c98f0b3ac6818e0e4924a11554c48d8ee1
parent 63a9735dd29b16c2d996cd3a01fac6b60cd626b7
Author: triesap <tyson@radroots.org>
Date:   Sun, 23 Aug 2026 18:40:42 +0000

contracts: freeze RHI v1 configuration

Diffstat:
MCargo.lock | 373+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
MCargo.toml | 2++
MREADME | 17+++++++++++++++++
Acontracts/services_hardening/config.v1.example.toml | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/services_hardening/config.v1.schema.json | 426+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mradroots.lib.source-lock.v1.toml | 2+-
Atests/services_hardening_config_contract.rs | 816+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 1755 insertions(+), 18 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -24,6 +24,20 @@ dependencies = [ ] [[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -33,6 +47,12 @@ dependencies = [ ] [[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -173,6 +193,21 @@ dependencies = [ ] [[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] name = "bitcoin-io" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -214,12 +249,24 @@ dependencies = [ ] [[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -316,7 +363,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -428,7 +475,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -455,6 +502,15 @@ dependencies = [ ] [[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -471,6 +527,17 @@ dependencies = [ ] [[package]] +name = "fancy-regex" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -493,12 +560,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -508,6 +592,16 @@ dependencies = [ ] [[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] name = "futures" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -613,9 +707,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -660,7 +756,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -670,6 +766,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -896,6 +1003,58 @@ dependencies = [ ] [[package]] +name = "jsonschema" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05a6cff806294a7699d48761d4ae2bdebab7fa2eef05e4a0b83320f0ff46901" +dependencies = [ + "ahash", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna 1.1.0", + "itoa", + "jsonschema-regex", + "jsonschema-value", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "serde", + "serde_json", + "strum", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84708dd3f6c5327478a96f37378adb27deed6a626cf84d19de780d9e8ae84b17" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "jsonschema-value" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8c04ab4ce60e42c4e302d3bd9d184652bb56f212495d2eea23573bcbf72556" +dependencies = [ + "ahash", + "bytecount", + "fraction", + "num-cmp", + "num-traits", + "serde_json", +] + +[[package]] name = "k256" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -978,6 +1137,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + +[[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1083,12 +1248,81 @@ dependencies = [ ] [[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[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" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1116,6 +1350,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1213,7 +1453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -1429,6 +1669,55 @@ dependencies = [ ] [[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "referencing" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980e77f5cfffc592c0e7dfb6d20770a0b96a6ab94d403de7454ed9799505e4ba" +dependencies = [ + "ahash", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1453,6 +1742,7 @@ dependencies = [ "chacha20poly1305", "clap", "futures-executor", + "jsonschema", "nostr", "nostr-sdk", "radroots_event", @@ -1473,6 +1763,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "url", "zeroize", ] @@ -1647,7 +1938,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1754,6 +2045,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1771,6 +2083,17 @@ dependencies = [ ] [[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1778,7 +2101,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1820,7 +2143,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1831,7 +2154,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1924,7 +2247,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2037,7 +2360,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2197,6 +2520,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2209,6 +2542,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2278,7 +2617,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2480,7 +2819,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2496,7 +2835,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2563,7 +2902,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2584,7 +2923,7 @@ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2604,7 +2943,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2644,7 +2983,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -54,3 +54,5 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } zeroize = { version = "1" } [dev-dependencies] +jsonschema = { version = "0.48.1", default-features = false } +url = "2" diff --git a/README b/README @@ -8,6 +8,23 @@ RHI publishes its service kind-0 Profile only through the sealed `RadrootsAuthoredProfile` replacement snapshot. It does not retain a generic kind-0 event-authoring path. +## Hardened v1 configuration contract + +The target service configuration is frozen by +`contracts/services_hardening/config.v1.schema.json` and the canonical +non-secret `config.v1.example.toml`. It is one strict immutable TOML document +with explicit identity, relay, evidence-source, reconciliation, attestation, +publication, presence, resource, and retention authority. The evidence fields +and maxima consume `evidence_policy.v1.json` without reinterpretation. Only +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. Existing environment and worker +selectors are prototype evidence, not authority to change or weaken the v1 +contract. + Use the repository-owned Nix lanes for validation: ```text diff --git a/contracts/services_hardening/config.v1.example.toml b/contracts/services_hardening/config.v1.example.toml @@ -0,0 +1,137 @@ +schema = "radroots.rhi.config" +schema_version = 1 + +[service] +shutdown_grace_ms = 30000 + +[logging] +level = "info" +format = "json" + +[operations] +enabled = false + +[database] +busy_timeout_ms = 5000 +max_connections = 8 +minimum_free_bytes = 268435456 + +[identity.service] +provider = "encrypted_file" +envelope_path = "/var/lib/radroots/services/rhi/default/secrets/service.identity.ncrypt" +credential_reference = "service_wrapping_key" +expected_public_key = "2222222222222222222222222222222222222222222222222222222222222222" + +[[relays]] +id = "relay-primary" +url = "wss://relay.example.com/" +read = true +write = true +required = true +authentication = "required" + +[[relays]] +id = "relay-secondary" +url = "wss://relay-secondary.example.com/" +read = false +write = true +required = false +authentication = "none" + +[network] +connect_deadline_ms = 10000 +dns_answer_limit = 16 + +[evidence] +contract = "radroots.rhi.evidence-policy" +contract_version = 1 +policy_id = "production-primary" + +[[evidence.sources]] +source_id = "trade-primary" +kind = "nostr_relay" +relay_id = "relay-primary" +required = true +selector = "trade_mutation_lineage_v1" +deadline_ms = 10000 +lookback_seconds = 86400 +overlap_seconds = 300 + +[reconciliation] +concurrency = 8 +queue_capacity = 4096 +lease_ms = 30000 +lease_renewal_ms = 10000 +max_attempts = 10 +initial_backoff_ms = 250 +maximum_backoff_ms = 30000 +attempt_deadline_ms = 30000 + +[attestation] +contract = "radroots.rhi.evidence_attestation.v1" +contract_version = 1 +method = "signed_evidence_snapshot" +reducer_contract = "radroots.trade.reducer.v1" +reducer_contract_version = 1 + +[publication] +mode = "required" +target_relay_ids = ["relay-primary", "relay-secondary"] + +[publication.retry] +max_attempts = 10 +initial_backoff_ms = 250 +maximum_backoff_ms = 30000 +attempt_deadline_ms = 15000 + +[presence] +enabled = true +profile = true +application_handler = true +target_relay_ids = ["relay-primary", "relay-secondary"] + +[resource_limits.admin] +header_count = 32 +header_bytes = 16384 +request_body_utf8_bytes = 65536 +response_body_utf8_bytes = 1048576 +concurrent_connections = 32 +request_deadline_ms = 15000 +idle_timeout_ms = 30000 +query_items = 100 + +[resource_limits.events] +wire_bytes = 262144 +content_bytes = 131072 +tag_count = 1024 +tag_total_elements = 4096 +tag_element_bytes = 4096 +tag_total_bytes = 131072 + +[resource_limits.source_results] +events = 4096 +bytes = 8388608 + +[resource_limits.queues] +ingress = 1024 +reconciliation = 4096 +publication = 4096 +presence = 64 + +[resource_limits.metrics] +descriptors = 64 +samples = 512 +labels_per_sample = 8 +render_utf8_bytes = 1048576 + +[resource_limits.runtime] +worker_threads = 4 +blocking_threads = 8 + +[retention] +duplicate_observations_ms = 604800000 +completed_jobs_ms = 2592000000 +terminal_publication_attempts_ms = 2592000000 +terminal_presence_attempts_ms = 2592000000 +operation_dedup_ms = 604800000 +audit_ms = 31536000000 diff --git a/contracts/services_hardening/config.v1.schema.json b/contracts/services_hardening/config.v1.schema.json @@ -0,0 +1,426 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://radroots.org/contracts/rhi/config.v1.schema.json", + "title": "Radroots RHI configuration v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "schema_version", + "service", + "logging", + "operations", + "database", + "identity", + "relays", + "network", + "evidence", + "reconciliation", + "attestation", + "publication", + "presence", + "resource_limits", + "retention" + ], + "properties": { + "schema": { "const": "radroots.rhi.config" }, + "schema_version": { "const": 1 }, + "service": { "$ref": "#/$defs/service" }, + "logging": { "$ref": "#/$defs/logging" }, + "operations": { "$ref": "#/$defs/operations" }, + "database": { "$ref": "#/$defs/database" }, + "identity": { "$ref": "#/$defs/identity" }, + "relays": { + "type": "array", + "minItems": 1, + "maxItems": 32, + "items": { "$ref": "#/$defs/relay" } + }, + "network": { "$ref": "#/$defs/network" }, + "evidence": { "$ref": "#/$defs/evidence" }, + "reconciliation": { "$ref": "#/$defs/reconciliation" }, + "attestation": { "$ref": "#/$defs/attestation" }, + "publication": { "$ref": "#/$defs/publication" }, + "presence": { "$ref": "#/$defs/presence" }, + "resource_limits": { "$ref": "#/$defs/resource_limits" }, + "retention": { "$ref": "#/$defs/retention" } + }, + "$defs": { + "stable_id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_-]*$" + }, + "contract_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-z][a-z0-9._-]*$" + }, + "absolute_path": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "pattern": "^/" + }, + "credential_reference": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[a-z][a-z0-9_-]*$" + }, + "public_key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "pattern": "^[0-9a-f]{64}$" + }, + "relay_id": { "$ref": "#/$defs/stable_id" }, + "relay_url": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "pattern": "^(wss|ws)://" + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "shutdown_grace_ms": { + "type": "integer", + "minimum": 1000, + "maximum": 300000, + "default": 30000, + "x-radroots-default-source": "accepted_service_authority" + } + } + }, + "logging": { + "type": "object", + "additionalProperties": false, + "properties": { + "level": { + "enum": ["error", "warn", "info", "debug", "trace"], + "default": "info", + "x-radroots-default-source": "engineering_safety" + }, + "format": { + "enum": ["json", "text"], + "default": "json", + "x-radroots-default-source": "engineering_safety" + } + } + }, + "operations_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "header_count": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" }, + "header_bytes": { "type": "integer", "minimum": 8192, "maximum": 32768, "default": 16384, "x-radroots-default-source": "radroots_service_host" }, + "response_body_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" }, + "concurrent_connections": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" }, + "request_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 15000, "x-radroots-default-source": "radroots_service_host" }, + "idle_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 30000, "x-radroots-default-source": "radroots_service_host" } + } + }, + "operations": { + "type": "object", + "additionalProperties": false, + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" }, + "listen": { "type": "string", "minLength": 1, "maxLength": 256 }, + "bind_policy": { "enum": ["loopback_only", "explicit_public"] }, + "limits": { "$ref": "#/$defs/operations_limits" } + }, + "allOf": [ + { + "if": { "properties": { "enabled": { "const": true } }, "required": ["enabled"] }, + "then": { "required": ["listen", "bind_policy", "limits"] }, + "else": { "maxProperties": 1 } + } + ] + }, + "database": { + "type": "object", + "additionalProperties": false, + "required": ["minimum_free_bytes"], + "properties": { + "busy_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 5000, "x-radroots-default-source": "radroots_service_sqlite" }, + "max_connections": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8, "x-radroots-default-source": "radroots_service_sqlite" }, + "minimum_free_bytes": { "type": "integer", "minimum": 1, "maximum": 9223372036854775807 } + } + }, + "encrypted_file_identity": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "envelope_path", "credential_reference", "expected_public_key"], + "properties": { + "provider": { "const": "encrypted_file" }, + "envelope_path": { "$ref": "#/$defs/absolute_path" }, + "credential_reference": { "$ref": "#/$defs/credential_reference" }, + "expected_public_key": { "$ref": "#/$defs/public_key" } + } + }, + "identity": { + "type": "object", + "additionalProperties": false, + "required": ["service"], + "properties": { "service": { "$ref": "#/$defs/encrypted_file_identity" } } + }, + "relay": { + "type": "object", + "additionalProperties": false, + "required": ["id", "url", "read", "write", "required", "authentication"], + "properties": { + "id": { "$ref": "#/$defs/relay_id" }, + "url": { "$ref": "#/$defs/relay_url" }, + "read": { "type": "boolean" }, + "write": { "type": "boolean" }, + "required": { "type": "boolean" }, + "authentication": { "enum": ["none", "required"] } + } + }, + "network": { + "type": "object", + "additionalProperties": false, + "properties": { + "connect_deadline_ms": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 10000, "x-radroots-default-source": "engineering_safety" }, + "dns_answer_limit": { "type": "integer", "minimum": 1, "maximum": 64, "default": 16, "x-radroots-default-source": "engineering_safety" } + } + }, + "evidence_source": { + "type": "object", + "additionalProperties": false, + "required": ["source_id", "kind", "relay_id", "required", "selector", "deadline_ms", "lookback_seconds", "overlap_seconds"], + "properties": { + "source_id": { "$ref": "#/$defs/stable_id" }, + "kind": { "const": "nostr_relay" }, + "relay_id": { "$ref": "#/$defs/relay_id" }, + "required": { "type": "boolean" }, + "selector": { "const": "trade_mutation_lineage_v1" }, + "deadline_ms": { "type": "integer", "minimum": 100, "maximum": 30000 }, + "lookback_seconds": { "type": "integer", "minimum": 60, "maximum": 2678400 }, + "overlap_seconds": { "type": "integer", "minimum": 1, "maximum": 86400 } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["contract", "contract_version", "policy_id", "sources"], + "properties": { + "contract": { "const": "radroots.rhi.evidence-policy" }, + "contract_version": { "const": 1 }, + "policy_id": { "$ref": "#/$defs/stable_id" }, + "sources": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { "$ref": "#/$defs/evidence_source" } + } + } + }, + "reconciliation": { + "type": "object", + "additionalProperties": false, + "properties": { + "concurrency": { "type": "integer", "minimum": 1, "maximum": 64, "default": 8, "x-radroots-default-source": "engineering_safety" }, + "queue_capacity": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4096, "x-radroots-default-source": "engineering_safety" }, + "lease_ms": { "type": "integer", "minimum": 1000, "maximum": 300000, "default": 30000, "x-radroots-default-source": "engineering_safety" }, + "lease_renewal_ms": { "type": "integer", "minimum": 100, "maximum": 150000, "default": 10000, "x-radroots-default-source": "engineering_safety" }, + "max_attempts": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10, "x-radroots-default-source": "engineering_safety" }, + "initial_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 250, "x-radroots-default-source": "engineering_safety" }, + "maximum_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 3600000, "default": 30000, "x-radroots-default-source": "engineering_safety" }, + "attempt_deadline_ms": { "type": "integer", "minimum": 100, "maximum": 300000, "default": 30000, "x-radroots-default-source": "engineering_safety" } + } + }, + "attestation": { + "type": "object", + "additionalProperties": false, + "required": ["contract", "contract_version", "method", "reducer_contract", "reducer_contract_version"], + "properties": { + "contract": { "const": "radroots.rhi.evidence_attestation.v1" }, + "contract_version": { "const": 1 }, + "method": { "const": "signed_evidence_snapshot" }, + "reducer_contract": { "const": "radroots.trade.reducer.v1" }, + "reducer_contract_version": { "const": 1 } + } + }, + "retry": { + "type": "object", + "additionalProperties": false, + "properties": { + "max_attempts": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10, "x-radroots-default-source": "engineering_safety" }, + "initial_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 250, "x-radroots-default-source": "engineering_safety" }, + "maximum_backoff_ms": { "type": "integer", "minimum": 1, "maximum": 3600000, "default": 30000, "x-radroots-default-source": "engineering_safety" }, + "attempt_deadline_ms": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 15000, "x-radroots-default-source": "engineering_safety" } + } + }, + "publication": { + "type": "object", + "additionalProperties": false, + "required": ["mode"], + "properties": { + "mode": { "enum": ["required", "disabled"] }, + "target_relay_ids": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/relay_id" } }, + "retry": { "$ref": "#/$defs/retry" } + }, + "allOf": [ + { + "if": { "properties": { "mode": { "const": "required" } }, "required": ["mode"] }, + "then": { "required": ["target_relay_ids", "retry"] }, + "else": { "maxProperties": 1 } + } + ] + }, + "presence": { + "type": "object", + "additionalProperties": false, + "required": ["enabled", "profile", "application_handler"], + "properties": { + "enabled": { "type": "boolean" }, + "profile": { "type": "boolean" }, + "application_handler": { "type": "boolean" }, + "target_relay_ids": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/relay_id" } } + }, + "allOf": [ + { + "if": { "properties": { "enabled": { "const": true } }, "required": ["enabled"] }, + "then": { "required": ["target_relay_ids"] }, + "else": { "maxProperties": 3 } + } + ] + }, + "admin_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "header_count": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" }, + "header_bytes": { "type": "integer", "minimum": 8192, "maximum": 32768, "default": 16384, "x-radroots-default-source": "radroots_service_host" }, + "request_body_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 65536, "x-radroots-default-source": "radroots_service_host" }, + "response_body_utf8_bytes": { "type": "integer", "minimum": 512, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" }, + "concurrent_connections": { "type": "integer", "minimum": 1, "maximum": 64, "default": 32, "x-radroots-default-source": "radroots_service_host" }, + "request_deadline_ms": { "type": "integer", "minimum": 1, "maximum": 30000, "default": 15000, "x-radroots-default-source": "radroots_service_host" }, + "idle_timeout_ms": { "type": "integer", "minimum": 1, "maximum": 60000, "default": 30000, "x-radroots-default-source": "radroots_service_host" }, + "query_items": { "type": "integer", "minimum": 1, "maximum": 200, "default": 100, "x-radroots-default-source": "accepted_service_authority" } + } + }, + "event_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "wire_bytes": { "type": "integer", "minimum": 1, "maximum": 524288, "default": 262144, "x-radroots-default-source": "radroots_event" }, + "content_bytes": { "type": "integer", "minimum": 1, "maximum": 131072, "default": 131072, "x-radroots-default-source": "radroots_event" }, + "tag_count": { "type": "integer", "minimum": 1, "maximum": 1024, "default": 1024, "x-radroots-default-source": "radroots_event" }, + "tag_total_elements": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 4096, "x-radroots-default-source": "radroots_event" }, + "tag_element_bytes": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 4096, "x-radroots-default-source": "radroots_event" }, + "tag_total_bytes": { "type": "integer", "minimum": 1, "maximum": 131072, "default": 131072, "x-radroots-default-source": "radroots_event" } + } + }, + "source_result_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "events": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 4096, "x-radroots-default-source": "rhi_evidence_policy" }, + "bytes": { "type": "integer", "minimum": 1, "maximum": 8388608, "default": 8388608, "x-radroots-default-source": "rhi_evidence_policy" } + } + }, + "queue_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "ingress": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 1024, "x-radroots-default-source": "engineering_safety" }, + "reconciliation": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4096, "x-radroots-default-source": "engineering_safety" }, + "publication": { "type": "integer", "minimum": 1, "maximum": 65536, "default": 4096, "x-radroots-default-source": "engineering_safety" }, + "presence": { "type": "integer", "minimum": 1, "maximum": 4096, "default": 64, "x-radroots-default-source": "engineering_safety" } + } + }, + "metrics_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "descriptors": { "type": "integer", "minimum": 1, "maximum": 64, "default": 64, "x-radroots-default-source": "radroots_service_host" }, + "samples": { "type": "integer", "minimum": 1, "maximum": 512, "default": 512, "x-radroots-default-source": "radroots_service_host" }, + "labels_per_sample": { "type": "integer", "minimum": 1, "maximum": 8, "default": 8, "x-radroots-default-source": "radroots_service_host" }, + "render_utf8_bytes": { "type": "integer", "minimum": 1, "maximum": 1048576, "default": 1048576, "x-radroots-default-source": "radroots_service_host" } + } + }, + "runtime_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "worker_threads": { "type": "integer", "minimum": 2, "maximum": 32, "default": 4, "x-radroots-default-source": "engineering_safety" }, + "blocking_threads": { "type": "integer", "minimum": 1, "maximum": 32, "default": 8, "x-radroots-default-source": "engineering_safety" } + } + }, + "resource_limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "admin": { "$ref": "#/$defs/admin_limits" }, + "events": { "$ref": "#/$defs/event_limits" }, + "source_results": { "$ref": "#/$defs/source_result_limits" }, + "queues": { "$ref": "#/$defs/queue_limits" }, + "metrics": { "$ref": "#/$defs/metrics_limits" }, + "runtime": { "$ref": "#/$defs/runtime_limits" } + } + }, + "retention": { + "type": "object", + "additionalProperties": false, + "required": ["duplicate_observations_ms", "completed_jobs_ms", "terminal_publication_attempts_ms", "terminal_presence_attempts_ms", "operation_dedup_ms", "audit_ms"], + "properties": { + "duplicate_observations_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 }, + "completed_jobs_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 }, + "terminal_publication_attempts_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 }, + "terminal_presence_attempts_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 }, + "operation_dedup_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 }, + "audit_ms": { "type": "integer", "minimum": 1000, "maximum": 31536000000 } + } + } + }, + "x-radroots-contract": { + "schema": "radroots.rhi.configuration-contract.v1", + "contract_version": 1, + "document_format": "toml_utf8", + "document_max_utf8_bytes": 1048576, + "unknown_fields": "reject_at_every_object_boundary", + "duplicate_keys": "reject_on_original_wire", + "null_values": "reject_on_original_wire", + "environment_overlay": "forbidden", + "protected_material": "forbidden", + "effective_output": "deterministic_redacted_with_exact_provenance", + "bootstrap_only": ["profile", "instance", "repo_local_root", "config_path"], + "evidence_policy": "contracts/services_hardening/evidence_policy.v1.json", + "evidence_policy_sha256": "b4da4aa2863905975c577668bc75cbb03857106a67899278130915e085319dc8", + "safe_default_sources": ["radroots_service_host", "radroots_service_sqlite", "radroots_event", "rhi_evidence_policy", "accepted_service_authority", "engineering_safety"], + "semantic_relationships": [ + "relay_ids_unique", + "relay_urls_canonical_unique", + "relay_read_or_write", + "production_relays_wss_only", + "repo_local_plaintext_relays_loopback_only", + "identity_envelope_path_is_canonical_absolute_non_root", + "configured_service_public_key_is_valid_x_only_secp256k1", + "evidence_source_ids_unique", + "evidence_source_bindings_unique", + "evidence_source_relays_reference_read_relays", + "at_least_one_required_evidence_source", + "evidence_overlap_not_greater_than_lookback", + "evidence_completion_is_nostr_eose_before_deadline", + "source_result_limits_match_evidence_policy_maxima", + "reconciliation_renewal_less_than_lease", + "reconciliation_initial_backoff_not_greater_than_maximum_backoff", + "reconciliation_attempt_deadline_not_less_than_source_deadlines", + "publication_targets_reference_write_relays", + "publication_disabled_has_no_targets_or_retry", + "publication_initial_backoff_not_greater_than_maximum_backoff", + "presence_targets_reference_write_relays", + "presence_disabled_has_no_targets_and_no_desired_documents", + "operations_public_bind_requires_explicit_public_policy", + "retention_preserves_current_and_reproducibility_authority" + ], + "forbidden_prototype_fields": ["paths", "storage", "workers", "worker", "output_dir", "state_file", "state_path", "relay_url", "env_file", "environment", "identity_path", "private_key", "secret_key", "wrapping_key_path", "sqlite_path", "json_state", "hot_reload", "include"] + } +} diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml @@ -6,4 +6,4 @@ workspace_catalog_sha256 = "8c27cebf6825f9ed74e122513c661f6dd31837dddb39d0988014 version = "0.1.0-alpha" source_archive_sha256 = "68badd1fb02d9396682d368e62dfe79969d8da48529760789c823fb4ab54aea3" lockfile = "Cargo.lock" -lockfile_sha256 = "9805ffe5e184cc377b41571c5d447a0b575b50a650e8b9b8b5df7059a567172f" +lockfile_sha256 = "3dda0cfcb08b133439343585c17429474cac1657dd97a7d46bfca3b04b3a8a87" diff --git a/tests/services_hardening_config_contract.rs b/tests/services_hardening_config_contract.rs @@ -0,0 +1,816 @@ +#![forbid(unsafe_code)] + +use std::collections::{BTreeMap, BTreeSet}; +use std::net::SocketAddr; +use std::path::{Component, Path}; + +use nostr::PublicKey; +use serde_json::{Map, Value, json}; + +const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json"); +const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); +const EVIDENCE_POLICY: &str = + include_str!("../contracts/services_hardening/evidence_policy.v1.json"); + +#[derive(Clone, Copy)] +enum Profile { + Production, + RepoLocal, +} + +fn schema() -> Value { + serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema must be valid JSON") +} + +fn evidence_policy() -> Value { + serde_json::from_str(EVIDENCE_POLICY).expect("evidence policy must be valid JSON") +} + +fn example() -> Value { + let value = toml::from_str::<toml::Value>(CONFIG_EXAMPLE) + .expect("configuration example must be valid TOML"); + serde_json::to_value(value).expect("TOML value must convert to JSON") +} + +fn schema_valid(value: &Value) -> bool { + jsonschema::validator_for(&schema()) + .expect("configuration schema must compile") + .is_valid(value) +} + +fn string_array<'a>(value: &'a Value, pointer: &str) -> Vec<&'a str> { + value + .pointer(pointer) + .and_then(Value::as_array) + .expect("string array") + .iter() + .map(|value| value.as_str().expect("string item")) + .collect() +} + +fn semantic_valid(value: &Value, profile: Profile) -> bool { + if !schema_valid(value) { + return false; + } + + let relays = value["relays"].as_array().expect("relays"); + let relay_ids = relays + .iter() + .map(|relay| relay["id"].as_str().expect("relay id")) + .collect::<Vec<_>>(); + let relay_urls = relays + .iter() + .map(|relay| relay["url"].as_str().expect("relay URL")) + .collect::<Vec<_>>(); + let parsed_relay_urls = relay_urls + .iter() + .map(|raw| { + url::Url::parse(raw) + .ok() + .filter(|parsed| parsed.as_str() == *raw) + }) + .collect::<Vec<_>>(); + if relay_ids.iter().collect::<BTreeSet<_>>().len() != relay_ids.len() + || relay_urls.iter().collect::<BTreeSet<_>>().len() != relay_urls.len() + || parsed_relay_urls.iter().any(Option::is_none) + || parsed_relay_urls.iter().flatten().any(|url| { + !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() + }) + || relays.iter().any(|relay| { + !relay["read"].as_bool().expect("read") && !relay["write"].as_bool().expect("write") + }) + { + return false; + } + if parsed_relay_urls.iter().flatten().any(|url| match profile { + Profile::Production => url.scheme() != "wss", + Profile::RepoLocal => match url.scheme() { + "wss" => false, + "ws" => !matches!( + url.host_str(), + Some("127.0.0.1" | "localhost" | "[::1]" | "::1") + ), + _ => true, + }, + }) { + return false; + } + let relays_by_id = relays + .iter() + .map(|relay| (relay["id"].as_str().expect("relay id"), relay)) + .collect::<BTreeMap<_, _>>(); + + let envelope_path = value["identity"]["service"]["envelope_path"] + .as_str() + .expect("envelope path"); + let path = Path::new(envelope_path); + if envelope_path.len() > 4096 + || path == Path::new("/") + || !path.is_absolute() + || path + .components() + .any(|component| matches!(component, Component::CurDir | Component::ParentDir)) + { + return false; + } + + let Ok(public_key) = PublicKey::from_hex( + value["identity"]["service"]["expected_public_key"] + .as_str() + .expect("public key"), + ) else { + return false; + }; + if public_key.xonly().is_err() { + return false; + } + + let sources = value["evidence"]["sources"] + .as_array() + .expect("evidence sources"); + let source_ids = sources + .iter() + .map(|source| source["source_id"].as_str().expect("source id")) + .collect::<Vec<_>>(); + let source_bindings = sources + .iter() + .map(|source| { + ( + source["kind"].as_str().expect("kind"), + source["relay_id"].as_str().expect("relay id"), + source["selector"].as_str().expect("selector"), + ) + }) + .collect::<Vec<_>>(); + if source_ids.iter().collect::<BTreeSet<_>>().len() != source_ids.len() + || source_bindings.iter().collect::<BTreeSet<_>>().len() != source_bindings.len() + || !sources + .iter() + .any(|source| source["required"].as_bool() == Some(true)) + || sources.iter().any(|source| { + source["overlap_seconds"].as_u64() > source["lookback_seconds"].as_u64() + || relays_by_id + .get(source["relay_id"].as_str().expect("relay id")) + .and_then(|relay| relay["read"].as_bool()) + != Some(true) + }) + { + return false; + } + + let reconciliation = &value["reconciliation"]; + let lease_ms = reconciliation["lease_ms"].as_u64().unwrap_or(30_000); + let lease_renewal_ms = reconciliation["lease_renewal_ms"] + .as_u64() + .unwrap_or(10_000); + let initial_backoff_ms = reconciliation["initial_backoff_ms"].as_u64().unwrap_or(250); + let maximum_backoff_ms = reconciliation["maximum_backoff_ms"] + .as_u64() + .unwrap_or(30_000); + let attempt_deadline_ms = reconciliation["attempt_deadline_ms"] + .as_u64() + .unwrap_or(30_000); + if lease_renewal_ms >= lease_ms + || initial_backoff_ms > maximum_backoff_ms + || sources + .iter() + .any(|source| source["deadline_ms"].as_u64() > Some(attempt_deadline_ms)) + { + return false; + } + + let publication = &value["publication"]; + if publication["mode"] == "required" + && (string_array(value, "/publication/target_relay_ids") + .iter() + .any(|id| { + relays_by_id + .get(id) + .and_then(|relay| relay["write"].as_bool()) + != Some(true) + }) + || publication["retry"]["initial_backoff_ms"] + .as_u64() + .unwrap_or(250) + > publication["retry"]["maximum_backoff_ms"] + .as_u64() + .unwrap_or(30_000)) + { + return false; + } + + let presence = &value["presence"]; + if presence["enabled"] == true { + if !presence["profile"].as_bool().unwrap_or(false) + && !presence["application_handler"].as_bool().unwrap_or(false) + { + return false; + } + if string_array(value, "/presence/target_relay_ids") + .iter() + .any(|id| { + relays_by_id + .get(id) + .and_then(|relay| relay["write"].as_bool()) + != Some(true) + }) + { + return false; + } + } else if presence["profile"] != false || presence["application_handler"] != false { + return false; + } + + let operations = &value["operations"]; + if operations["enabled"] == true { + let Ok(listen) = operations["listen"] + .as_str() + .unwrap_or_default() + .parse::<SocketAddr>() + else { + return false; + }; + if listen.port() == 0 + || (operations["bind_policy"] == "loopback_only" && !listen.ip().is_loopback()) + { + return false; + } + } + true +} + +fn assert_rejected(value: &Value, profile: Profile) { + assert!( + !semantic_valid(value, profile), + "negative configuration vector unexpectedly passed" + ); +} + +fn insert(value: &mut Value, pointer: &str, key: &str, replacement: Value) { + value + .pointer_mut(pointer) + .and_then(Value::as_object_mut) + .expect("object pointer") + .insert(key.to_owned(), replacement); +} + +fn defaults(value: &Value, pointer: &str, output: &mut BTreeMap<String, Value>) { + match value { + Value::Object(object) => { + if let Some(default) = object.get("default") { + assert!( + object.contains_key("x-radroots-default-source"), + "default without provenance at {pointer}" + ); + output.insert(pointer.to_owned(), default.clone()); + } + for (key, child) in object { + defaults(child, &format!("{pointer}/{key}"), output); + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + defaults(child, &format!("{pointer}/{index}"), output); + } + } + _ => {} + } +} + +fn assert_integer_bounds(value: &Value, pointer: &str) { + match value { + Value::Object(object) => { + if object.get("type") == Some(&Value::String("integer".to_owned())) { + assert!( + object.contains_key("minimum"), + "missing minimum at {pointer}" + ); + assert!( + object.contains_key("maximum"), + "missing maximum at {pointer}" + ); + } + for (key, child) in object { + assert_integer_bounds(child, &format!("{pointer}/{key}")); + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + assert_integer_bounds(child, &format!("{pointer}/{index}")); + } + } + _ => {} + } +} + +fn assert_object_schemas_are_closed(value: &Value, pointer: &str) { + match value { + Value::Object(object) => { + if object.get("type") == Some(&Value::String("object".to_owned())) { + assert_eq!( + object.get("additionalProperties"), + Some(&Value::Bool(false)), + "open object schema at {pointer}" + ); + } + for (key, child) in object { + assert_object_schemas_are_closed(child, &format!("{pointer}/{key}")); + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + assert_object_schemas_are_closed(child, &format!("{pointer}/{index}")); + } + } + _ => {} + } +} + +#[test] +fn schema_identity_structure_and_machine_policy_are_exact() { + let schema = schema(); + assert_eq!( + schema["$schema"], + "https://json-schema.org/draft/2020-12/schema" + ); + assert_eq!(schema["type"], "object"); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["required"], + json!([ + "schema", + "schema_version", + "service", + "logging", + "operations", + "database", + "identity", + "relays", + "network", + "evidence", + "reconciliation", + "attestation", + "publication", + "presence", + "resource_limits", + "retention" + ]) + ); + assert_eq!( + schema["properties"]["schema"]["const"], + "radroots.rhi.config" + ); + assert_eq!(schema["properties"]["schema_version"]["const"], 1); + assert_eq!( + schema["x-radroots-contract"]["bootstrap_only"], + json!(["profile", "instance", "repo_local_root", "config_path"]) + ); + assert_eq!( + schema["x-radroots-contract"]["document_max_utf8_bytes"], + 1_048_576 + ); + assert_eq!( + schema["x-radroots-contract"]["duplicate_keys"], + "reject_on_original_wire" + ); + assert_eq!( + schema["x-radroots-contract"]["null_values"], + "reject_on_original_wire" + ); + assert_eq!( + schema["x-radroots-contract"]["environment_overlay"], + "forbidden" + ); + assert_eq!( + schema["x-radroots-contract"]["protected_material"], + "forbidden" + ); + assert_eq!( + schema["x-radroots-contract"]["effective_output"], + "deterministic_redacted_with_exact_provenance" + ); + assert_eq!( + schema["x-radroots-contract"]["evidence_policy_sha256"], + "b4da4aa2863905975c577668bc75cbb03857106a67899278130915e085319dc8" + ); + assert_integer_bounds(&schema, ""); + assert_object_schemas_are_closed(&schema, ""); + + let mut found_defaults = BTreeMap::new(); + defaults(&schema, "", &mut found_defaults); + assert!(!found_defaults.is_empty()); + for pointer in found_defaults.keys() { + let source = schema.pointer(pointer).expect("default pointer")["x-radroots-default-source"] + .as_str() + .expect("default source"); + assert!( + schema["x-radroots-contract"]["safe_default_sources"] + .as_array() + .expect("safe default sources") + .iter() + .any(|allowed| allowed == source) + ); + } +} + +#[test] +fn evidence_policy_is_consumed_without_reinterpretation() { + let schema = schema(); + let policy = evidence_policy(); + assert_eq!( + policy["configuration"]["contract"], + schema["$defs"]["evidence"]["properties"]["contract"]["const"] + ); + assert_eq!( + policy["configuration"]["contract_version"], + schema["$defs"]["evidence"]["properties"]["contract_version"]["const"] + ); + assert_eq!( + policy["configuration"]["sources"]["minimum_items"], + schema["$defs"]["evidence"]["properties"]["sources"]["minItems"] + ); + assert_eq!( + policy["configuration"]["sources"]["maximum_items"], + schema["$defs"]["evidence"]["properties"]["sources"]["maxItems"] + ); + for field in ["deadline_ms", "lookback_seconds", "overlap_seconds"] { + assert_eq!( + policy["configuration"]["source"][field]["minimum"], + schema["$defs"]["evidence_source"]["properties"][field]["minimum"] + ); + assert_eq!( + policy["configuration"]["source"][field]["maximum"], + schema["$defs"]["evidence_source"]["properties"][field]["maximum"] + ); + } + assert_eq!( + policy["source_kinds"]["qualified_v1"], + json!(["nostr_relay"]) + ); + assert_eq!(policy["selector"]["id"], "trade_mutation_lineage_v1"); + assert_eq!( + policy["completion"]["success_evidence"], + "nostr_eose_received_before_source_deadline" + ); + assert_eq!( + policy["selector"]["maximum_events_per_result"], + schema["$defs"]["source_result_limits"]["properties"]["events"]["maximum"] + ); + assert_eq!( + policy["selector"]["maximum_result_bytes"], + schema["$defs"]["source_result_limits"]["properties"]["bytes"]["maximum"] + ); +} + +#[test] +fn canonical_example_and_required_positive_variants_pass() { + let value = example(); + let validator = jsonschema::validator_for(&schema()).expect("schema"); + let schema_errors = validator + .iter_errors(&value) + .map(|error| error.to_string()) + .collect::<Vec<_>>(); + assert!(schema_errors.is_empty(), "{schema_errors:?}"); + assert!(semantic_valid(&value, Profile::Production)); + assert_eq!(value["identity"]["service"]["provider"], "encrypted_file"); + assert_eq!( + value["attestation"], + json!({ + "contract": "radroots.rhi.evidence_attestation.v1", + "contract_version": 1, + "method": "signed_evidence_snapshot", + "reducer_contract": "radroots.trade.reducer.v1", + "reducer_contract_version": 1 + }) + ); + assert_eq!( + value["evidence"], + evidence_policy()["configuration_fixture"] + ); + + let mut publication_disabled = value.clone(); + publication_disabled["publication"] = json!({"mode": "disabled"}); + assert!(semantic_valid(&publication_disabled, Profile::Production)); + + let mut presence_disabled = value.clone(); + presence_disabled["presence"] = json!({ + "enabled": false, + "profile": false, + "application_handler": false + }); + assert!(semantic_valid(&presence_disabled, Profile::Production)); + + let mut repo_local = value.clone(); + repo_local["relays"][0]["url"] = json!("ws://127.0.0.1:8080/"); + repo_local["relays"][1]["url"] = json!("ws://localhost:8081/"); + assert!(semantic_valid(&repo_local, Profile::RepoLocal)); + assert_rejected(&repo_local, Profile::Production); + + let mut enabled_operations = value; + enabled_operations["operations"] = json!({ + "enabled": true, + "listen": "127.0.0.1:9460", + "bind_policy": "loopback_only", + "limits": {} + }); + assert!(semantic_valid(&enabled_operations, Profile::Production)); + + let mut sparse_retry = example(); + sparse_retry["publication"]["retry"] = json!({"initial_backoff_ms": 250}); + assert!(semantic_valid(&sparse_retry, Profile::Production)); +} + +#[test] +fn structural_unknown_legacy_and_original_wire_vectors_fail() { + let value = example(); + for (pointer, key) in [ + ("", "unknown"), + ("/service", "shutdowm_grace_ms"), + ("/identity/service", "wrapping_key_path"), + ("/evidence/sources/0", "completion"), + ("/resource_limits/admin", "body_bytes"), + ] { + let mut invalid = value.clone(); + insert(&mut invalid, pointer, key, json!(true)); + assert_rejected(&invalid, Profile::Production); + } + for field in schema()["x-radroots-contract"]["forbidden_prototype_fields"] + .as_array() + .expect("forbidden fields") + { + let mut invalid = value.clone(); + invalid.as_object_mut().expect("config object").insert( + field.as_str().expect("field").to_owned(), + Value::Object(Map::new()), + ); + assert_rejected(&invalid, Profile::Production); + } + assert!(toml::from_str::<toml::Value>("schema='a'\nschema='b'\n").is_err()); + assert!( + toml::from_str::<toml::Value>( + "[service]\nshutdown_grace_ms=30000\nshutdown_grace_ms=30001\n" + ) + .is_err() + ); + assert!(toml::from_str::<toml::Value>("schema = null\n").is_err()); +} + +#[test] +fn identity_relay_and_network_authority_fail_closed() { + let value = example(); + let mut relative = value.clone(); + relative["identity"]["service"]["envelope_path"] = json!("relative/path"); + assert_rejected(&relative, Profile::Production); + for path in ["/", "/var/lib/radroots/../escape.ncrypt"] { + let mut invalid = value.clone(); + invalid["identity"]["service"]["envelope_path"] = json!(path); + assert_rejected(&invalid, Profile::Production); + } + for provider in [ + "plaintext_file", + "external_command", + "keyring", + "managed_account", + ] { + let mut invalid = value.clone(); + invalid["identity"]["service"]["provider"] = json!(provider); + assert_rejected(&invalid, Profile::Production); + } + let mut invalid = value.clone(); + invalid["identity"]["service"]["expected_public_key"] = json!("f".repeat(64)); + assert_rejected(&invalid, Profile::Production); + let mut duplicate_id = value.clone(); + duplicate_id["relays"][1]["id"] = duplicate_id["relays"][0]["id"].clone(); + assert_rejected(&duplicate_id, Profile::Production); + let mut duplicate_url = value.clone(); + duplicate_url["relays"][1]["url"] = duplicate_url["relays"][0]["url"].clone(); + assert_rejected(&duplicate_url, Profile::Production); + let mut inactive = value.clone(); + inactive["relays"][0]["read"] = json!(false); + inactive["relays"][0]["write"] = json!(false); + assert_rejected(&inactive, Profile::Production); + let mut insecure = value.clone(); + insecure["relays"][0]["url"] = json!("ws://127.0.0.1:8080/"); + assert_rejected(&insecure, Profile::Production); + let mut noncanonical = value; + noncanonical["relays"][0]["url"] = json!("WSS://relay.example.com"); + assert_rejected(&noncanonical, Profile::Production); +} + +#[test] +fn evidence_scope_bounds_and_relationships_fail_closed() { + let value = example(); + for (pointer, minimum, below, maximum, above) in [ + ("/evidence/sources/0/deadline_ms", 100, 99, 30_000, 30_001), + ( + "/evidence/sources/0/lookback_seconds", + 60, + 59, + 2_678_400, + 2_678_401, + ), + ("/evidence/sources/0/overlap_seconds", 1, 0, 86_400, 86_401), + ] { + for valid in [minimum, maximum] { + let mut candidate = value.clone(); + *candidate.pointer_mut(pointer).expect("bounded field") = json!(valid); + if pointer.ends_with("lookback_seconds") && valid == 60 { + candidate["evidence"]["sources"][0]["overlap_seconds"] = json!(60); + } + assert!(semantic_valid(&candidate, Profile::Production), "{pointer}"); + } + for invalid in [below, above] { + let mut candidate = value.clone(); + *candidate.pointer_mut(pointer).expect("bounded field") = json!(invalid); + assert_rejected(&candidate, Profile::Production); + } + } + let mut no_required = value.clone(); + no_required["evidence"]["sources"][0]["required"] = json!(false); + assert_rejected(&no_required, Profile::Production); + let mut overlap = value.clone(); + overlap["evidence"]["sources"][0]["lookback_seconds"] = json!(60); + overlap["evidence"]["sources"][0]["overlap_seconds"] = json!(61); + assert_rejected(&overlap, Profile::Production); + let mut missing_relay = value.clone(); + missing_relay["evidence"]["sources"][0]["relay_id"] = json!("missing"); + assert_rejected(&missing_relay, Profile::Production); + let mut write_only = value.clone(); + write_only["relays"][0]["read"] = json!(false); + assert_rejected(&write_only, Profile::Production); + for replacement in [json!("filesystem"), json!("publication_target")] { + let mut invalid = value.clone(); + invalid["evidence"]["sources"][0]["kind"] = replacement; + assert_rejected(&invalid, Profile::Production); + } + let mut selector = value.clone(); + selector["evidence"]["sources"][0]["selector"] = json!("all_events_v1"); + assert_rejected(&selector, Profile::Production); + let mut seventeen = value; + let source = seventeen["evidence"]["sources"][0].clone(); + seventeen["evidence"]["sources"] = Value::Array(vec![source; 17]); + assert_rejected(&seventeen, Profile::Production); +} + +#[test] +fn reconciliation_publication_presence_and_operations_fail_closed() { + let value = example(); + let mut renewal = value.clone(); + renewal["reconciliation"]["lease_renewal_ms"] = renewal["reconciliation"]["lease_ms"].clone(); + assert_rejected(&renewal, Profile::Production); + let mut backoff = value.clone(); + backoff["reconciliation"]["initial_backoff_ms"] = json!(30_001); + assert_rejected(&backoff, Profile::Production); + let mut attempt = value.clone(); + attempt["reconciliation"]["attempt_deadline_ms"] = json!(9_999); + assert_rejected(&attempt, Profile::Production); + let mut publication_relay = value.clone(); + publication_relay["publication"]["target_relay_ids"] = json!(["missing"]); + assert_rejected(&publication_relay, Profile::Production); + let mut disabled_leak = value.clone(); + disabled_leak["publication"]["mode"] = json!("disabled"); + assert_rejected(&disabled_leak, Profile::Production); + let mut publication_backoff = value.clone(); + publication_backoff["publication"]["retry"]["initial_backoff_ms"] = json!(30_001); + assert_rejected(&publication_backoff, Profile::Production); + let mut presence_relay = value.clone(); + presence_relay["presence"]["target_relay_ids"] = json!(["missing"]); + assert_rejected(&presence_relay, Profile::Production); + let mut no_desired_presence = value.clone(); + no_desired_presence["presence"]["profile"] = json!(false); + no_desired_presence["presence"]["application_handler"] = json!(false); + assert_rejected(&no_desired_presence, Profile::Production); + let mut disabled_presence_leak = value.clone(); + disabled_presence_leak["presence"]["enabled"] = json!(false); + assert_rejected(&disabled_presence_leak, Profile::Production); + let mut public_loopback_policy = value; + public_loopback_policy["operations"] = json!({ + "enabled": true, + "listen": "0.0.0.0:9460", + "bind_policy": "loopback_only", + "limits": {} + }); + assert_rejected(&public_loopback_policy, Profile::Production); +} + +#[test] +fn exact_resource_boundaries_and_safe_defaults_are_frozen() { + let value = example(); + for (pointer, maximum, over) in [ + ("/database/busy_timeout_ms", 60_000_u64, 60_001_u64), + ("/database/max_connections", 8, 9), + ("/resource_limits/admin/header_count", 64, 65), + ("/resource_limits/admin/query_items", 200, 201), + ("/resource_limits/events/wire_bytes", 524_288, 524_289), + ("/resource_limits/source_results/events", 4_096, 4_097), + ( + "/resource_limits/source_results/bytes", + 8_388_608, + 8_388_609, + ), + ("/resource_limits/metrics/samples", 512, 513), + ("/resource_limits/runtime/worker_threads", 32, 33), + ] { + let mut minimum = value.clone(); + *minimum.pointer_mut(pointer).expect("bounded field") = json!(1); + if pointer.ends_with("worker_threads") { + *minimum.pointer_mut(pointer).expect("bounded field") = json!(2); + } + assert!( + semantic_valid(&minimum, Profile::Production), + "minimum {pointer}" + ); + let mut exact = value.clone(); + *exact.pointer_mut(pointer).expect("bounded field") = json!(maximum); + assert!( + semantic_valid(&exact, Profile::Production), + "maximum {pointer}" + ); + let mut zero = value.clone(); + *zero.pointer_mut(pointer).expect("bounded field") = json!(0); + assert_rejected(&zero, Profile::Production); + let mut excessive = value.clone(); + *excessive.pointer_mut(pointer).expect("bounded field") = json!(over); + assert_rejected(&excessive, Profile::Production); + } + let mut exact_admin_response = value.clone(); + exact_admin_response["resource_limits"]["admin"]["response_body_utf8_bytes"] = json!(512); + assert!(semantic_valid(&exact_admin_response, Profile::Production)); + let mut undersized = value.clone(); + undersized["resource_limits"]["admin"]["response_body_utf8_bytes"] = json!(511); + assert_rejected(&undersized, Profile::Production); + + let mut omitted = value; + omitted["service"] = json!({}); + omitted["logging"] = json!({}); + omitted["network"] = json!({}); + omitted["reconciliation"] = json!({}); + omitted["resource_limits"] = json!({}); + omitted["database"] + .as_object_mut() + .expect("database") + .remove("busy_timeout_ms"); + omitted["database"] + .as_object_mut() + .expect("database") + .remove("max_connections"); + assert!(semantic_valid(&omitted, Profile::Production)); +} + +#[test] +fn schema_version_authority_and_protected_material_are_closed() { + let value = example(); + for replacement in [json!("radroots.rhi.config.v2"), json!("rhi")] { + let mut invalid = value.clone(); + invalid["schema"] = replacement; + assert_rejected(&invalid, Profile::Production); + } + for replacement in [json!(0), json!(2), json!("1")] { + let mut invalid = value.clone(); + invalid["schema_version"] = replacement; + assert_rejected(&invalid, Profile::Production); + } + for required in [ + "minimum_free_bytes", + "identity", + "relays", + "evidence", + "sources", + "deadline_ms", + "lookback_seconds", + "overlap_seconds", + "reconciliation", + "attestation", + "publication", + "presence", + "resource_limits", + "retention", + ] { + assert!( + CONFIG_SCHEMA.contains(required), + "missing authority {required}" + ); + } + let lowercase = CONFIG_EXAMPLE.to_ascii_lowercase(); + for forbidden in [ + "private_key", + "secret_key", + "mnemonic", + "nsec1", + "wrapping_key =", + "wrapping_key_path", + "rhi_", + "env_file", + "plaintext_file", + "keyring", + "managed_account", + "worker_root", + "state_file", + "json_state", + ] { + assert!( + !lowercase.contains(forbidden), + "forbidden material {forbidden}" + ); + } +}