commit 7b757841e8ad137ddaf57851ab4dd92de1bd36fd
parent 42439e555d3ac65ea572aa990081a508a571960c
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 13:36:17 +0000
refactor: consume the release facade only
- replace private runtime composition with the radroots facade
- keep local health and profile inspection artifact-backed
- fail closed for operations without final SDK orchestration
- remove retired engine source and sibling dependency paths
Diffstat:
64 files changed, 698 insertions(+), 34968 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,69 +3,6 @@
version = 4
[[package]]
-name = "adler2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-
-[[package]]
-name = "aead"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
-dependencies = [
- "crypto-common",
- "generic-array",
-]
-
-[[package]]
-name = "aes"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures 0.2.17",
-]
-
-[[package]]
-name = "ahash"
-version = "0.8.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -101,7 +38,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]]
@@ -112,22 +49,10 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys 0.61.2",
+ "windows-sys",
]
[[package]]
-name = "anyhow"
-version = "1.0.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
-
-[[package]]
-name = "arraydeque"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
-
-[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -149,69 +74,6 @@ dependencies = [
]
[[package]]
-name = "async-trait"
-version = "0.1.91"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 3.0.3",
-]
-
-[[package]]
-name = "async-utility"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9"
-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"
-checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
-dependencies = [
- "num-traits",
-]
-
-[[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"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
-[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -225,84 +87,15 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
-version = "0.21.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
-
-[[package]]
-name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
-name = "base64ct"
-version = "1.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
-
-[[package]]
-name = "bech32"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
-
-[[package]]
-name = "bip39"
-version = "2.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
-dependencies = [
- "bitcoin_hashes",
- "serde",
- "unicode-normalization",
-]
-
-[[package]]
-name = "bitcoin-consensus-encoding"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9"
-dependencies = [
- "bitcoin-internals",
- "hex-conservative 1.2.0",
- "serde",
-]
-
-[[package]]
-name = "bitcoin-internals"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5"
-
-[[package]]
-name = "bitcoin-io"
-version = "0.1.101"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78"
-dependencies = [
- "bitcoin-consensus-encoding",
-]
-
-[[package]]
-name = "bitcoin_hashes"
-version = "0.14.101"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2"
-dependencies = [
- "bitcoin-io",
- "hex-conservative 0.2.2",
- "serde",
-]
-
-[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
-dependencies = [
- "serde_core",
-]
[[package]]
name = "block-buffer"
@@ -314,15 +107,6 @@ dependencies = [
]
[[package]]
-name = "block-padding"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
name = "bstr"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -340,27 +124,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
-name = "byteorder"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
-
-[[package]]
-name = "bytes"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
-
-[[package]]
-name = "cbc"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
-dependencies = [
- "cipher",
-]
-
-[[package]]
name = "cc"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -377,71 +140,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "cfg_aliases"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
-
-[[package]]
-name = "chacha20"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
-dependencies = [
- "cfg-if",
- "cipher",
- "cpufeatures 0.2.17",
-]
-
-[[package]]
-name = "chacha20"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "rand_core 0.10.1",
-]
-
-[[package]]
-name = "chacha20poly1305"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
-dependencies = [
- "aead",
- "chacha20 0.9.1",
- "cipher",
- "poly1305",
- "zeroize",
-]
-
-[[package]]
-name = "chrono"
-version = "0.4.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
-name = "cipher"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
-dependencies = [
- "crypto-common",
- "inout",
- "zeroize",
-]
-
-[[package]]
name = "clap"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -488,3177 +186,907 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
-name = "config"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
-dependencies = [
- "async-trait",
- "convert_case",
- "json5",
- "nom",
- "pathdiff",
- "ron",
- "rust-ini",
- "serde",
- "serde_json",
- "toml",
- "yaml-rust2",
-]
-
-[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
-name = "const-random"
-version = "0.1.18"
+name = "cpufeatures"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
- "const-random-macro",
+ "libc",
]
[[package]]
-name = "const-random-macro"
-version = "0.1.16"
+name = "crypto-bigint"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
- "getrandom 0.2.17",
- "once_cell",
- "tiny-keccak",
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
]
[[package]]
-name = "convert_case"
-version = "0.6.0"
+name = "crypto-common"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
- "unicode-segmentation",
+ "generic-array",
+ "typenum",
]
[[package]]
-name = "core-foundation"
-version = "0.9.4"
+name = "der"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
- "core-foundation-sys",
- "libc",
+ "const-oid",
+ "zeroize",
]
[[package]]
-name = "core-foundation"
-version = "0.10.1"
+name = "difflib"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
[[package]]
-name = "cpufeatures"
-version = "0.2.17"
+name = "displaydoc"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
- "libc",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
]
[[package]]
-name = "cpufeatures"
-version = "0.3.0"
+name = "elliptic-curve"
+version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
- "libc",
+ "base16ct",
+ "crypto-bigint",
+ "ff",
+ "generic-array",
+ "group",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
]
[[package]]
-name = "crc"
-version = "3.4.0"
+name = "errno"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
- "crc-catalog",
+ "libc",
+ "windows-sys",
]
[[package]]
-name = "crc-catalog"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
-name = "crc32fast"
-version = "1.5.0"
+name = "fallible-streaming-iterator"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
-dependencies = [
- "cfg-if",
-]
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
-name = "crossbeam-channel"
-version = "0.5.16"
+name = "fastrand"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
-dependencies = [
- "crossbeam-utils",
-]
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
-name = "crossbeam-queue"
-version = "0.3.13"
+name = "ff"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
- "crossbeam-utils",
+ "rand_core",
+ "subtle",
]
[[package]]
-name = "crossbeam-utils"
-version = "0.8.22"
+name = "find-msvc-tools"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
-name = "crunchy"
-version = "0.2.4"
+name = "foldhash"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
-name = "crypto-bigint"
-version = "0.5.5"
+name = "form_urlencoded"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "subtle",
- "zeroize",
+ "percent-encoding",
]
[[package]]
-name = "crypto-common"
-version = "0.1.7"
+name = "fs2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
- "generic-array",
- "rand_core 0.6.4",
- "typenum",
+ "libc",
+ "winapi",
]
[[package]]
-name = "data-encoding"
-version = "2.11.1"
+name = "generic-array"
+version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
[[package]]
-name = "dbus"
-version = "0.9.12"
+name = "getrandom"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
+ "cfg-if",
"libc",
- "libdbus-sys",
- "windows-sys 0.61.2",
+ "r-efi",
]
[[package]]
-name = "dbus-secret-service"
-version = "4.1.0"
+name = "group"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
- "dbus",
- "openssl",
- "zeroize",
+ "ff",
+ "rand_core",
+ "subtle",
]
[[package]]
-name = "der"
-version = "0.7.10"
+name = "hashbrown"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
- "const-oid",
- "zeroize",
+ "foldhash",
]
[[package]]
-name = "deranged"
-version = "0.5.8"
+name = "heck"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
-name = "difflib"
-version = "0.4.0"
+name = "hex"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
-name = "digest"
-version = "0.10.7"
+name = "icu_collections"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
- "block-buffer",
- "crypto-common",
- "subtle",
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
]
[[package]]
-name = "displaydoc"
-version = "0.2.7"
+name = "icu_locale_core"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 3.0.3",
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
]
[[package]]
-name = "dlv-list"
-version = "0.5.2"
+name = "icu_normalizer"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
- "const-random",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
]
[[package]]
-name = "dotenvy"
-version = "0.15.7"
+name = "icu_normalizer_data"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
-name = "either"
-version = "1.17.0"
+name = "icu_properties"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
- "serde",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
]
[[package]]
-name = "elliptic-curve"
-version = "0.13.8"
+name = "icu_properties_data"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
-dependencies = [
- "base16ct",
- "crypto-bigint",
- "ff",
- "generic-array",
- "group",
- "rand_core 0.6.4",
- "sec1",
- "subtle",
- "zeroize",
-]
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
-name = "encoding_rs"
-version = "0.8.35"
+name = "icu_provider"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
- "cfg-if",
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
]
[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "errno"
-version = "0.3.14"
+name = "idna"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
- "libc",
- "windows-sys 0.61.2",
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
]
[[package]]
-name = "event-listener"
-version = "5.4.2"
+name = "idna_adapter"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
- "parking",
- "pin-project-lite",
+ "icu_normalizer",
+ "icu_properties",
]
[[package]]
-name = "fallible-iterator"
-version = "0.3.0"
+name = "is_terminal_polyfill"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
-name = "fallible-streaming-iterator"
-version = "0.1.9"
+name = "itoa"
+version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
-name = "fastrand"
-version = "2.5.0"
+name = "jiff-tzdb"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
[[package]]
-name = "ff"
-version = "0.13.1"
+name = "js-sys"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
- "rand_core 0.6.4",
- "subtle",
+ "cfg-if",
+ "wasm-bindgen",
]
[[package]]
-name = "filetime"
-version = "0.2.29"
+name = "k256"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
dependencies = [
"cfg-if",
- "libc",
+ "elliptic-curve",
]
[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
+name = "libc"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
-name = "flate2"
-version = "1.1.9"
+name = "libsqlite3-sys"
+version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
dependencies = [
- "crc32fast",
- "miniz_oxide",
+ "cc",
+ "pkg-config",
+ "vcpkg",
]
[[package]]
-name = "flume"
-version = "0.12.0"
+name = "linux-raw-sys"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
-dependencies = [
- "futures-core",
- "futures-sink",
- "spin",
-]
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
-name = "foldhash"
-version = "0.2.0"
+name = "litemap"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
-name = "foreign-types"
-version = "0.3.2"
+name = "mediatype"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
+checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7"
[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
+name = "memchr"
+version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
+name = "num-traits"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
- "percent-encoding",
+ "autocfg",
]
[[package]]
-name = "fs2"
-version = "0.4.3"
+name = "once_cell"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "futures"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-intrusive"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
-dependencies = [
- "futures-core",
- "lock_api",
- "parking_lot",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
-
-[[package]]
-name = "futures-sink"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
-
-[[package]]
-name = "futures-task"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
-
-[[package]]
-name = "futures-util"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "slab",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
- "zeroize",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi 5.3.0",
- "wasip2",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "r-efi 6.0.0",
- "rand_core 0.10.1",
- "wasm-bindgen",
-]
-
-[[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"
-checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
-dependencies = [
- "ff",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.14.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.16.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
-dependencies = [
- "allocator-api2",
- "equivalent",
- "foldhash",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
-
-[[package]]
-name = "hashlink"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
-dependencies = [
- "hashbrown 0.14.5",
-]
-
-[[package]]
-name = "hashlink"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
-dependencies = [
- "hashbrown 0.16.1",
-]
-
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hex"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-
-[[package]]
-name = "hex-conservative"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
-dependencies = [
- "arrayvec",
-]
-
-[[package]]
-name = "hex-conservative"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10"
-dependencies = [
- "arrayvec",
-]
-
-[[package]]
-name = "hmac"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "http"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
-dependencies = [
- "bytes",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
-dependencies = [
- "bytes",
- "http",
-]
-
-[[package]]
-name = "http-body-util"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
-dependencies = [
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "pin-project-lite",
-]
-
-[[package]]
-name = "httparse"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
-
-[[package]]
-name = "hyper"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
-dependencies = [
- "atomic-waker",
- "bytes",
- "futures-channel",
- "futures-core",
- "http",
- "http-body",
- "httparse",
- "itoa",
- "pin-project-lite",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.27.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
-dependencies = [
- "http",
- "hyper",
- "hyper-util",
- "rustls",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "webpki-roots 1.0.9",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-channel",
- "futures-util",
- "http",
- "http-body",
- "hyper",
- "ipnet",
- "libc",
- "percent-encoding",
- "pin-project-lite",
- "socket2",
- "tokio",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "icu_collections"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
-dependencies = [
- "displaydoc",
- "potential_utf",
- "utf8_iter",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locale_core"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
-dependencies = [
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
-
-[[package]]
-name = "icu_properties"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
-dependencies = [
- "icu_collections",
- "icu_locale_core",
- "icu_properties_data",
- "icu_provider",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
-
-[[package]]
-name = "icu_provider"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
-dependencies = [
- "displaydoc",
- "icu_locale_core",
- "writeable",
- "yoke",
- "zerofrom",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "idna"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
-dependencies = [
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[package]]
-name = "idna"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
-[[package]]
-name = "indexmap"
-version = "2.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
-dependencies = [
- "equivalent",
- "hashbrown 0.17.1",
-]
-
-[[package]]
-name = "inout"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
-dependencies = [
- "block-padding",
- "generic-array",
-]
-
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
-name = "ipnet"
-version = "2.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
-
-[[package]]
-name = "is_terminal_polyfill"
-version = "1.70.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
-
-[[package]]
-name = "itoa"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
-
-[[package]]
-name = "jiff-tzdb"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
-
-[[package]]
-name = "js-sys"
-version = "0.3.103"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
-dependencies = [
- "cfg-if",
- "futures-util",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "json5"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
-dependencies = [
- "pest",
- "pest_derive",
- "serde",
-]
-
-[[package]]
-name = "k256"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
-dependencies = [
- "cfg-if",
- "elliptic-curve",
-]
-
-[[package]]
-name = "keyring"
-version = "3.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
-dependencies = [
- "byteorder",
- "dbus-secret-service",
- "linux-keyutils",
- "log",
- "openssl",
- "security-framework 2.11.1",
- "security-framework 3.7.0",
- "windows-sys 0.60.2",
- "zeroize",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-[[package]]
-name = "libc"
-version = "0.2.189"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
-
-[[package]]
-name = "libdbus-sys"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
-dependencies = [
- "cc",
- "pkg-config",
-]
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.37.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
-dependencies = [
- "cc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "linux-keyutils"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590"
-dependencies = [
- "bitflags",
- "libc",
-]
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
-
-[[package]]
-name = "litemap"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
-
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
-
-[[package]]
-name = "lru"
-version = "0.16.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
-
-[[package]]
-name = "lru-slab"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
-
-[[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"
-checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7"
-
-[[package]]
-name = "memchr"
-version = "2.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
- "simd-adler32",
-]
-
-[[package]]
-name = "mio"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
-dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "negentropy"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
-name = "nostr"
-version = "0.44.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7d3d987ea7078dc36947cde532637c472a229426702e4331dd7667325378bd9"
-dependencies = [
- "aes",
- "base64 0.22.1",
- "bech32",
- "bip39",
- "bitcoin_hashes",
- "cbc",
- "chacha20 0.9.1",
- "chacha20poly1305",
- "getrandom 0.2.17",
- "hex",
- "instant",
- "scrypt",
- "secp256k1",
- "serde",
- "serde_json",
- "unicode-normalization",
- "url",
- "url-fork",
-]
-
-[[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-conv"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
-
-[[package]]
-name = "once_cell_polyfill"
-version = "1.70.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
-
-[[package]]
-name = "opaque-debug"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
-
-[[package]]
-name = "openssl"
-version = "0.10.81"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
-dependencies = [
- "bitflags",
- "cfg-if",
- "foreign-types",
- "libc",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
-name = "openssl-src"
-version = "300.6.1+3.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.117"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
-dependencies = [
- "cc",
- "libc",
- "openssl-src",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "ordered-multimap"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
-dependencies = [
- "dlv-list",
- "hashbrown 0.14.5",
-]
-
-[[package]]
-name = "parking"
-version = "2.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
-name = "password-hash"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
-dependencies = [
- "base64ct",
- "rand_core 0.6.4",
- "subtle",
-]
-
-[[package]]
-name = "pathdiff"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
-
-[[package]]
-name = "pbkdf2"
-version = "0.12.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
-dependencies = [
- "digest",
- "hmac",
-]
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
-
-[[package]]
-name = "pest"
-version = "2.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2"
-dependencies = [
- "memchr",
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c"
-dependencies = [
- "pest",
-]
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
-
-[[package]]
-name = "pkg-config"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
-
-[[package]]
-name = "poly1305"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
-dependencies = [
- "cpufeatures 0.2.17",
- "opaque-debug",
- "universal-hash",
-]
-
-[[package]]
-name = "potential_utf"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
-dependencies = [
- "zerovec",
-]
-
-[[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"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
-dependencies = [
- "zerocopy",
-]
-
-[[package]]
-name = "predicates"
-version = "3.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
-dependencies = [
- "anstyle",
- "difflib",
- "predicates-core",
-]
-
-[[package]]
-name = "predicates-core"
-version = "1.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
-
-[[package]]
-name = "predicates-tree"
-version = "1.0.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
-dependencies = [
- "predicates-core",
- "termtree",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.107"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quinn"
-version = "0.11.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
-dependencies = [
- "bytes",
- "cfg_aliases",
- "pin-project-lite",
- "quinn-proto",
- "quinn-udp",
- "rustc-hash",
- "rustls",
- "socket2",
- "thiserror 2.0.19",
- "tokio",
- "tracing",
- "web-time",
-]
-
-[[package]]
-name = "quinn-proto"
-version = "0.11.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
-dependencies = [
- "bytes",
- "getrandom 0.4.3",
- "lru-slab",
- "rand 0.10.2",
- "rand_pcg",
- "ring",
- "rustc-hash",
- "rustls",
- "rustls-pki-types",
- "slab",
- "thiserror 2.0.19",
- "tinyvec",
- "tracing",
- "web-time",
-]
-
-[[package]]
-name = "quinn-udp"
-version = "0.5.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
-dependencies = [
- "cfg_aliases",
- "libc",
- "once_cell",
- "socket2",
- "tracing",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.47"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "r-efi"
-version = "5.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
-
-[[package]]
-name = "r-efi"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-
-[[package]]
-name = "radroots_blossom"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9088b79d81fc7e89a8ec14592b2c176000e25bf7e04a91f2cf188f2b3059842d"
-dependencies = [
- "mediatype",
- "serde",
- "sha2",
- "unicode-general-category",
- "url",
-]
-
-[[package]]
-name = "radroots_cli"
-version = "0.1.0"
-dependencies = [
- "assert_cmd",
- "chacha20poly1305",
- "chrono",
- "clap",
- "flate2",
- "getrandom 0.2.17",
- "nostr",
- "nostr-sdk",
- "radroots_core",
- "radroots_event",
- "radroots_event_codec",
- "radroots_identity",
- "radroots_log",
- "radroots_mesh",
- "radroots_nostr",
- "radroots_nostr_accounts",
- "radroots_nostr_connect",
- "radroots_protected_store",
- "radroots_protocol",
- "radroots_replica_schema",
- "radroots_replica_store",
- "radroots_replica_sync",
- "radroots_runtime",
- "radroots_runtime_contract_v1",
- "radroots_runtime_paths",
- "radroots_runtime_store",
- "radroots_sdk",
- "radroots_secret_vault",
- "radroots_signing",
- "radroots_sql_core",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_sync",
- "radroots_trade",
- "radroots_transport",
- "radroots_transport_nostr",
- "serde",
- "serde_json",
- "tar",
- "tempfile",
- "thiserror 2.0.19",
- "tokio",
- "toml",
- "tungstenite",
- "url",
- "zeroize",
-]
-
-[[package]]
-name = "radroots_core"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e025474387307ad31b9badba61d40a6780772c76bf797d108802394fc454ace3"
-dependencies = [
- "rust_decimal",
- "serde",
-]
-
-[[package]]
-name = "radroots_event"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02af0e49a14deed13c93b35f10705c7dd6f4abbe70c925a67af614d55decf3a3"
-dependencies = [
- "hex",
- "jiff-tzdb",
- "radroots_blossom",
- "radroots_core",
- "radroots_identity",
- "radroots_protocol",
- "serde",
- "serde_json",
- "sha2",
- "unicode-general-category",
- "url",
-]
-
-[[package]]
-name = "radroots_event_codec"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "730541f773566e0adf46705f36be06f6441410c2e1c9da5b147e2aa7c004eb20"
-dependencies = [
- "radroots_blossom",
- "radroots_core",
- "radroots_event",
- "radroots_identity",
- "radroots_protocol",
- "secp256k1",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "radroots_geonames"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb32a26e5e06f15cbf79e1556873e456a2e0d4a4bb01eec22bc3e2a97db03960"
-dependencies = [
- "fs2",
- "rusqlite",
- "sha2",
- "tempfile",
- "url",
-]
-
-[[package]]
-name = "radroots_identity"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bc11dc4df1afba04add840f7a1231bee15d989931808ea7ffef20a4277af9a6"
-dependencies = [
- "k256",
- "serde",
- "thiserror 2.0.19",
-]
-
-[[package]]
-name = "radroots_log"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "588181ad5ca25e01e775bf953c6861dd4545ea08c6db691df8a4eb753eca62f6"
-dependencies = [
- "chrono",
- "serde_json",
- "thiserror 1.0.69",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "radroots_mesh"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b4a4fc196644ae4b6d2cb023bcf4074883edfb7db8f0731b462dfecf49b96ad"
-dependencies = [
- "radroots_transport",
-]
-
-[[package]]
-name = "radroots_nostr"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d988d9ea535c33adb6f26704f8cc29170d07bb5fc16486a5abdb38c27e824dfa"
-dependencies = [
- "nostr",
- "radroots_event",
- "radroots_event_codec",
- "radroots_identity",
- "radroots_signing",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "radroots_nostr_accounts"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29fc51468b787b9578a6b3a18d3a6c11e15e40a37966734c3839199519419428"
-dependencies = [
- "nostr",
- "radroots_identity",
- "radroots_nostr_signer",
- "radroots_protected_store",
- "radroots_runtime",
- "radroots_secret_vault",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "zeroize",
-]
-
-[[package]]
-name = "radroots_nostr_connect"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80f998e609addddbd54dc139a93da734e331e80666cfb5d6d4ce6d80f3632a94"
-dependencies = [
- "nostr",
- "radroots_event",
- "radroots_identity",
- "radroots_nostr",
- "radroots_protocol",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
- "url",
-]
-
-[[package]]
-name = "radroots_nostr_signer"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08e7f193bbf32f4f14c620de249defe242b3ef61439c0b1f1bd6625fdaf49037"
-dependencies = [
- "hex",
- "nostr",
- "radroots_identity",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_runtime",
- "serde",
- "serde_json",
- "sha2",
- "thiserror 1.0.69",
- "url",
- "uuid",
-]
-
-[[package]]
-name = "radroots_protected_store"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9715a7c7b7c259257393e2486f3f5ffc34891d958e8bf1a11e403d7d5484a6e"
-dependencies = [
- "chacha20poly1305",
- "getrandom 0.2.17",
- "radroots_secret_vault",
- "serde",
- "serde_json",
- "zeroize",
-]
-
-[[package]]
-name = "radroots_protocol"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbada19f5ddf856da0df008f54e5ebf37737863f5df067c26d4ac40ce5aa18"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "radroots_protocol_contract_v1"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12f718ed748702f7f2b00af58bc6a6cb1b2030fe7f3b8bbcd7835bfe6741f66e"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "radroots_replica_schema"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c17d02275bd4e5e97e9bd9571bdbe778da73a94dfb531de2656917a0869d998d"
-dependencies = [
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "radroots_replica_store"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d159430b5ffda4abd0129fad668c733860c26408b06fdec0f00ec08b8961e297"
-dependencies = [
- "hex",
- "radroots_replica_schema",
- "radroots_sql_core",
- "serde",
- "serde_json",
- "sha2",
-]
-
-[[package]]
-name = "radroots_replica_sync"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34a043971a295e66d5db56ec347da11467e2ca1f83c67d910c2242c756158a5d"
-dependencies = [
- "base64 0.22.1",
- "hex",
- "radroots_core",
- "radroots_event",
- "radroots_event_codec",
- "radroots_replica_schema",
- "radroots_replica_store",
- "radroots_sql_core",
- "serde",
- "serde_json",
- "sha2",
- "uuid",
-]
-
-[[package]]
-name = "radroots_runtime"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b35b19bdc24751b0cc0a2ab835ea94a31d0409fec2ea53b9438430f1f351c9d"
-dependencies = [
- "anyhow",
- "chacha20poly1305",
- "config",
- "getrandom 0.2.17",
- "radroots_log",
- "radroots_protected_store",
- "radroots_runtime_paths",
- "radroots_secret_vault",
- "serde",
- "serde_json",
- "tempfile",
- "thiserror 1.0.69",
- "tokio",
- "toml",
- "tracing",
- "zeroize",
-]
-
-[[package]]
-name = "radroots_runtime_contract_v1"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecb8d0655dac16f64b0df451cfa2c600d375934b2054c5a8822ad7637dd3083e"
-dependencies = [
- "radroots_protocol_contract_v1",
- "serde",
-]
-
-[[package]]
-name = "radroots_runtime_paths"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a930344bed69f7f43625eb33969c02290be5fb69c424dda3e34fe9cbaa75c120"
-dependencies = [
- "serde",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "radroots_runtime_store"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a6a1f494999d2c4d1f64b293ec9d775a44136d8cd8d65bc0629fe035421f406"
-dependencies = [
- "radroots_sql_core",
- "serde",
- "serde_json",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "radroots_sdk"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d38a596e3aeff8f0529e446f5fa014b6519dcf8b7a4312d2836348119b0506f6"
-dependencies = [
- "radroots_core",
- "radroots_event",
- "radroots_event_codec",
- "radroots_geonames",
- "radroots_identity",
- "radroots_nostr",
- "radroots_nostr_connect",
- "radroots_protocol",
- "radroots_secrets",
- "radroots_signing",
- "radroots_storage",
- "radroots_storage_sqlite",
- "radroots_sync",
- "radroots_trade",
- "radroots_transport",
- "radroots_transport_nostr",
- "reqwest",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "radroots_secret_vault"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8f323939c546a0e680c916d33dfcf6092cc4f2ab8195c02007ead012b215653"
-dependencies = [
- "keyring",
-]
-
-[[package]]
-name = "radroots_secrets"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f586ada77097ae1d806d4cc966d5e8161e6e28e55e57935e989bb0b0a9a14ad"
-dependencies = [
- "chacha20poly1305",
- "zeroize",
-]
-
-[[package]]
-name = "radroots_signing"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69d5ed02d28a3e166f4e10c90a78b1c0eae7235ed39a145c9530617ab7e188d0"
-dependencies = [
- "radroots_event",
- "radroots_identity",
- "radroots_protocol",
- "serde",
-]
-
-[[package]]
-name = "radroots_sql_core"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f5bdb02012b0570df7411d87a1a61c87abd36dd0dca63fffefeed9525069f54"
-dependencies = [
- "chrono",
- "futures-executor",
- "serde",
- "serde_json",
- "sqlx",
- "uuid",
-]
-
-[[package]]
-name = "radroots_storage"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7b3118e52ab4c54a38739aa74465a6ae0ed57bd0138f8ea0f60a659fb72a6ee"
-dependencies = [
- "radroots_event",
- "radroots_protocol",
- "radroots_trade",
- "radroots_transport",
- "serde",
-]
-
-[[package]]
-name = "radroots_storage_sqlite"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d162bc66eb22ee13fbe7fc5eae02581bcff3ab062efac66a32cf334cf941210"
-dependencies = [
- "fs2",
- "radroots_event_codec",
- "radroots_secrets",
- "radroots_storage",
- "sha2",
- "sqlx",
-]
-
-[[package]]
-name = "radroots_sync"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fbb2beab3b3b33590e72b1ae759d46580ef92f645be3e65dff8dcca6249d8a0"
-dependencies = [
- "radroots_event",
- "radroots_event_codec",
- "radroots_protocol",
- "radroots_signing",
- "radroots_storage",
- "radroots_trade",
- "radroots_transport",
- "serde",
- "sha2",
-]
-
-[[package]]
-name = "radroots_trade"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baea4d0b37c59f04f063964c8696d4b3ad64eac26b374fc5c1d59808053bfc9e"
-dependencies = [
- "base64 0.22.1",
- "hex",
- "radroots_core",
- "radroots_event",
- "radroots_event_codec",
- "radroots_identity",
- "serde",
- "serde_json",
- "sha2",
-]
-
-[[package]]
-name = "radroots_transport"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcd67a15b7c49483534985dad0307c7318c9eca19b6f3005a7bf73dccdbc42a4"
-dependencies = [
- "radroots_event",
- "radroots_identity",
- "radroots_protocol",
- "serde",
- "sha2",
-]
-
-[[package]]
-name = "radroots_transport_nostr"
-version = "0.1.0-alpha"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71f7efee834ae72dfbc1bf8f99cd2cafe606d50eb9505c739774cfc543209584"
-dependencies = [
- "async-wsocket",
- "nostr-relay-pool",
- "nostr-sdk",
- "radroots_event_codec",
- "radroots_nostr",
- "radroots_protocol",
- "radroots_transport",
- "serde_json",
- "tokio",
- "tokio-tungstenite",
- "url",
-]
-
-[[package]]
-name = "rand"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
-dependencies = [
- "libc",
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
-dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.5",
-]
-
-[[package]]
-name = "rand"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
-dependencies = [
- "chacha20 0.10.1",
- "getrandom 0.4.3",
- "rand_core 0.10.1",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.5",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom 0.2.17",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
-dependencies = [
- "getrandom 0.3.4",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
-
-[[package]]
-name = "rand_pcg"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
-dependencies = [
- "rand_core 0.10.1",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
-
-[[package]]
-name = "reqwest"
-version = "0.12.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-rustls",
- "hyper-util",
- "js-sys",
- "log",
- "percent-encoding",
- "pin-project-lite",
- "quinn",
- "rustls",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper",
- "tokio",
- "tokio-rustls",
- "tower",
- "tower-http",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "webpki-roots 1.0.9",
-]
-
-[[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 = "ron"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
-dependencies = [
- "base64 0.21.7",
- "bitflags",
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "rsqlite-vfs"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
-dependencies = [
- "hashbrown 0.16.1",
- "thiserror 2.0.19",
-]
-
-[[package]]
-name = "rusqlite"
-version = "0.39.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e"
-dependencies = [
- "bitflags",
- "fallible-iterator",
- "fallible-streaming-iterator",
- "libsqlite3-sys",
- "smallvec",
- "sqlite-wasm-rs",
-]
-
-[[package]]
-name = "rust-ini"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
-dependencies = [
- "cfg-if",
- "ordered-multimap",
-]
-
-[[package]]
-name = "rust_decimal"
-version = "1.42.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a"
-dependencies = [
- "arrayvec",
- "num-traits",
- "serde",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
-
-[[package]]
-name = "rustix"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "rustls"
-version = "0.23.43"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
-dependencies = [
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
-dependencies = [
- "web-time",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-webpki"
-version = "0.103.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
-
-[[package]]
-name = "ryu"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
-
-[[package]]
-name = "salsa20"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
-dependencies = [
- "cipher",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "scrypt"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
-dependencies = [
- "password-hash",
- "pbkdf2",
- "salsa20",
- "sha2",
-]
-
-[[package]]
-name = "sec1"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
-dependencies = [
- "base16ct",
- "der",
- "generic-array",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "secp256k1"
-version = "0.29.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
-dependencies = [
- "rand 0.8.7",
- "secp256k1-sys",
- "serde",
-]
-
-[[package]]
-name = "secp256k1-sys"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "security-framework"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
-dependencies = [
- "bitflags",
- "core-foundation 0.9.4",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework"
-version = "3.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
-dependencies = [
- "bitflags",
- "core-foundation 0.10.1",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.229"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
-name = "serde_core"
-version = "1.0.229"
+name = "once_cell_polyfill"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
-dependencies = [
- "serde_derive",
-]
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
-name = "serde_derive"
-version = "1.0.229"
+name = "percent-encoding"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 3.0.3",
-]
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
-name = "serde_json"
-version = "1.0.151"
+name = "pin-project-lite"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
-dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
-]
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "serde_spanned"
-version = "0.6.9"
+name = "pkg-config"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
-dependencies = [
- "serde",
-]
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "potential_utf"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
+ "zerovec",
]
[[package]]
-name = "sha1"
-version = "0.10.7"
+name = "predicates"
+version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
dependencies = [
- "cfg-if",
- "cpufeatures 0.2.17",
- "digest",
+ "anstyle",
+ "difflib",
+ "predicates-core",
]
[[package]]
-name = "sha2"
-version = "0.10.9"
+name = "predicates-core"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.2.17",
- "digest",
-]
+checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
+name = "predicates-tree"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
dependencies = [
- "lazy_static",
+ "predicates-core",
+ "termtree",
]
[[package]]
-name = "shlex"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
-
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
+name = "proc-macro2"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
- "errno",
- "libc",
+ "unicode-ident",
]
[[package]]
-name = "simd-adler32"
-version = "0.3.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
-
-[[package]]
-name = "slab"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
-
-[[package]]
-name = "smallvec"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
-
-[[package]]
-name = "socket2"
-version = "0.6.5"
+name = "quote"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
- "libc",
- "windows-sys 0.61.2",
+ "proc-macro2",
]
[[package]]
-name = "spin"
-version = "0.9.9"
+name = "r-efi"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
-dependencies = [
- "lock_api",
-]
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
-name = "sqlite-wasm-rs"
-version = "0.5.5"
+name = "radroots"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
+checksum = "df239d81b548496352953820b7efcc240b35759a5eab2af4e52194ccd83593cd"
dependencies = [
- "cc",
- "js-sys",
- "rsqlite-vfs",
- "wasm-bindgen",
+ "radroots_core",
+ "radroots_event",
+ "radroots_identity",
+ "radroots_sdk",
+ "radroots_trade",
+ "radroots_transport",
]
[[package]]
-name = "sqlx"
-version = "0.9.0"
+name = "radroots_blossom"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71"
+checksum = "786aabfb3b884f1c01a2060c3bba57fed7d83ba6e7cddc2ce4e07d33fcc5a67d"
dependencies = [
- "sqlx-core",
- "sqlx-macros",
- "sqlx-sqlite",
+ "mediatype",
+ "serde",
+ "sha2",
+ "unicode-general-category",
+ "url",
]
[[package]]
-name = "sqlx-core"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb"
+name = "radroots_cli"
+version = "0.1.0"
dependencies = [
- "base64 0.22.1",
- "bytes",
- "cfg-if",
- "crc",
- "crossbeam-queue",
- "either",
- "event-listener",
- "futures-core",
- "futures-intrusive",
- "futures-io",
- "futures-util",
- "hashbrown 0.16.1",
- "hashlink 0.11.1",
- "indexmap",
- "log",
- "memchr",
- "percent-encoding",
- "serde",
- "sha2",
- "smallvec",
- "thiserror 2.0.19",
+ "assert_cmd",
+ "clap",
+ "radroots",
+ "serde_json",
"tokio",
- "tokio-stream",
- "tracing",
- "url",
]
[[package]]
-name = "sqlx-macros"
-version = "0.9.0"
+name = "radroots_core"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf"
+checksum = "e43ac2e488444ced1cddd6785d0a43552f887440c3f0506d9d2f90c7be30b26c"
dependencies = [
- "proc-macro2",
- "quote",
- "sqlx-core",
- "sqlx-macros-core",
- "syn 2.0.119",
+ "rust_decimal",
+ "serde",
]
[[package]]
-name = "sqlx-macros-core"
-version = "0.9.0"
+name = "radroots_event"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3"
+checksum = "9b1cfbf17b7c94ee0182c48042438ccc3c6c5979043a6ef300004935780408a2"
dependencies = [
- "cfg-if",
- "dotenvy",
- "either",
- "heck",
"hex",
- "proc-macro2",
- "quote",
+ "jiff-tzdb",
+ "radroots_blossom",
+ "radroots_core",
+ "radroots_identity",
+ "radroots_protocol",
"serde",
"serde_json",
"sha2",
- "sqlx-core",
- "sqlx-sqlite",
- "syn 2.0.119",
- "tokio",
+ "unicode-general-category",
"url",
]
[[package]]
-name = "sqlx-sqlite"
-version = "0.9.0"
+name = "radroots_event_codec"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c"
+checksum = "b3344dee67769f76754d8713312f57f54301e9eac00584c26c51fb1b1aab9042"
dependencies = [
- "atoi",
- "flume",
- "form_urlencoded",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-intrusive",
- "futures-util",
- "libsqlite3-sys",
- "log",
- "percent-encoding",
+ "radroots_blossom",
+ "radroots_core",
+ "radroots_event",
+ "radroots_identity",
+ "radroots_protocol",
+ "secp256k1",
"serde",
- "sqlx-core",
- "thiserror 2.0.19",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
-
-[[package]]
-name = "strsim"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-
-[[package]]
-name = "subtle"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-
-[[package]]
-name = "symlink"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
-
-[[package]]
-name = "syn"
-version = "2.0.119"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[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 = "sync_wrapper"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
-name = "tar"
-version = "0.4.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
-dependencies = [
- "filetime",
- "libc",
- "xattr",
+ "serde_json",
]
[[package]]
-name = "tempfile"
-version = "3.27.0"
+name = "radroots_geonames"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+checksum = "5964b621444c1c4748a9b3df8a82e8652364ed66962d694d8673ea186cf0712c"
dependencies = [
- "fastrand",
- "getrandom 0.4.3",
- "once_cell",
- "rustix",
- "windows-sys 0.61.2",
+ "fs2",
+ "rusqlite",
+ "sha2",
+ "tempfile",
+ "url",
]
[[package]]
-name = "termtree"
-version = "0.5.1"
+name = "radroots_identity"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+checksum = "95d406e0fa84faee0a87493358b4c45151271a12b0bc3d57aa0167b968680eea"
+dependencies = [
+ "k256",
+ "serde",
+ "thiserror",
+]
[[package]]
-name = "thiserror"
-version = "1.0.69"
+name = "radroots_protocol"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+checksum = "979c9e8217ccb11cfe1af3bf85145f3abcd4234dfc45f74801398598fd542d5a"
dependencies = [
- "thiserror-impl 1.0.69",
+ "serde",
]
[[package]]
-name = "thiserror"
-version = "2.0.19"
+name = "radroots_sdk"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+checksum = "0b791cce95667c25a19a13c19b3ae1e30d1ae5301b20d7195955ce0774076e48"
dependencies = [
- "thiserror-impl 2.0.19",
+ "radroots_core",
+ "radroots_event",
+ "radroots_event_codec",
+ "radroots_geonames",
+ "radroots_identity",
+ "radroots_protocol",
+ "radroots_signing",
+ "radroots_storage",
+ "radroots_trade",
+ "radroots_transport",
]
[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
+name = "radroots_signing"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+checksum = "a3fa5a21d9e3aa6910303b866bcbd466243b6a797d5005fb591cd6e497a06c0b"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
+ "radroots_event",
+ "radroots_identity",
+ "radroots_protocol",
]
[[package]]
-name = "thiserror-impl"
-version = "2.0.19"
+name = "radroots_storage"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+checksum = "591d16fa94a6a802b9c0c254b853af9a3d16fe16345432ab2a0a7908b707a37b"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 3.0.3",
+ "radroots_event",
+ "radroots_protocol",
+ "radroots_trade",
+ "radroots_transport",
]
[[package]]
-name = "thread_local"
-version = "1.1.10"
+name = "radroots_trade"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+checksum = "696aad8bd5fdd85e9d6f2c5c8ef4a35d6662ec7dfd654852fb3a76fe982734f5"
dependencies = [
- "cfg-if",
+ "base64",
+ "hex",
+ "radroots_core",
+ "radroots_event",
+ "radroots_event_codec",
+ "radroots_identity",
+ "serde",
+ "serde_json",
+ "sha2",
]
[[package]]
-name = "time"
-version = "0.3.55"
+name = "radroots_transport"
+version = "0.1.0-alpha"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+checksum = "e3b0adaa62f7649abb500b0e9b9d7d8f875c3ed42da780aea920776f1c5b7417"
dependencies = [
- "deranged",
- "num-conv",
- "powerfmt",
- "serde_core",
- "time-core",
- "time-macros",
+ "radroots_event",
+ "radroots_identity",
+ "radroots_protocol",
+ "sha2",
]
[[package]]
-name = "time-core"
-version = "0.1.9"
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
[[package]]
-name = "time-macros"
-version = "0.2.32"
+name = "rsqlite-vfs"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [
- "num-conv",
- "time-core",
+ "hashbrown",
+ "thiserror",
]
[[package]]
-name = "tiny-keccak"
-version = "2.0.2"
+name = "rusqlite"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e"
dependencies = [
- "crunchy",
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "libsqlite3-sys",
+ "smallvec",
+ "sqlite-wasm-rs",
]
[[package]]
-name = "tinystr"
-version = "0.8.3"
+name = "rust_decimal"
+version = "1.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a"
dependencies = [
- "displaydoc",
- "zerovec",
+ "arrayvec",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
]
[[package]]
-name = "tinyvec"
-version = "1.12.0"
+name = "rustix"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "tinyvec_macros",
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
]
[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
+name = "rustversion"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
-name = "tokio"
-version = "1.53.1"
+name = "sec1"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
- "bytes",
- "libc",
- "mio",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2",
- "tokio-macros",
- "windows-sys 0.61.2",
+ "base16ct",
+ "der",
+ "generic-array",
+ "subtle",
+ "zeroize",
]
[[package]]
-name = "tokio-macros"
-version = "2.7.2"
+name = "secp256k1"
+version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
- "proc-macro2",
- "quote",
- "syn 3.0.3",
+ "secp256k1-sys",
]
[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
+name = "secp256k1-sys"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
dependencies = [
- "rustls",
- "tokio",
+ "cc",
]
[[package]]
-name = "tokio-socks"
-version = "0.5.3"
+name = "serde"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
- "either",
- "futures-util",
- "thiserror 1.0.69",
- "tokio",
+ "serde_core",
+ "serde_derive",
]
[[package]]
-name = "tokio-stream"
-version = "0.1.19"
+name = "serde_core"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
- "futures-core",
- "pin-project-lite",
- "tokio",
+ "serde_derive",
]
[[package]]
-name = "tokio-tungstenite"
-version = "0.26.2"
+name = "serde_derive"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
- "futures-util",
- "log",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tungstenite",
- "webpki-roots 0.26.11",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
]
[[package]]
-name = "toml"
-version = "0.8.23"
+name = "serde_json"
+version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
+ "itoa",
+ "memchr",
"serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
+ "serde_core",
+ "zmij",
]
[[package]]
-name = "toml_datetime"
-version = "0.6.11"
+name = "sha2"
+version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
- "serde",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
]
[[package]]
-name = "toml_edit"
-version = "0.22.27"
+name = "shlex"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
-dependencies = [
- "indexmap",
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_write",
- "winnow",
-]
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
-name = "toml_write"
-version = "0.1.2"
+name = "smallvec"
+version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
-name = "tower"
-version = "0.5.3"
+name = "sqlite-wasm-rs"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
dependencies = [
- "futures-core",
- "futures-util",
- "pin-project-lite",
- "sync_wrapper",
- "tokio",
- "tower-layer",
- "tower-service",
+ "cc",
+ "js-sys",
+ "rsqlite-vfs",
+ "wasm-bindgen",
]
[[package]]
-name = "tower-http"
-version = "0.6.11"
+name = "stable_deref_trait"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
-dependencies = [
- "bitflags",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "pin-project-lite",
- "tower",
- "tower-layer",
- "tower-service",
- "url",
-]
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
-name = "tower-layer"
-version = "0.3.3"
+name = "strsim"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
-name = "tower-service"
-version = "0.3.3"
+name = "subtle"
+version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
-name = "tracing"
-version = "0.1.44"
+name = "syn"
+version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
- "log",
- "pin-project-lite",
- "tracing-attributes",
- "tracing-core",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "tracing-appender"
-version = "0.2.5"
+name = "syn"
+version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
- "crossbeam-channel",
- "symlink",
- "thiserror 2.0.19",
- "time",
- "tracing-subscriber",
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
]
[[package]]
-name = "tracing-attributes"
-version = "0.1.31"
+name = "synstructure"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
@@ -3666,67 +1094,73 @@ dependencies = [
]
[[package]]
-name = "tracing-core"
-version = "0.1.36"
+name = "tempfile"
+version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
+ "fastrand",
+ "getrandom",
"once_cell",
- "valuable",
+ "rustix",
+ "windows-sys",
]
[[package]]
-name = "tracing-log"
-version = "0.2.0"
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "thiserror"
+version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
dependencies = [
- "log",
- "once_cell",
- "tracing-core",
+ "thiserror-impl",
]
[[package]]
-name = "tracing-subscriber"
-version = "0.3.23"
+name = "thiserror-impl"
+version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex-automata",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
]
[[package]]
-name = "try-lock"
-version = "0.2.5"
+name = "tokio"
+version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "pin-project-lite",
+ "tokio-macros",
+]
[[package]]
-name = "tungstenite"
-version = "0.26.2"
+name = "tokio-macros"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
- "bytes",
- "data-encoding",
- "http",
- "httparse",
- "log",
- "rand 0.9.5",
- "rustls",
- "rustls-pki-types",
- "sha1",
- "thiserror 2.0.19",
- "utf-8",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
]
[[package]]
@@ -3736,18 +1170,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
-name = "ucd-trie"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
-
-[[package]]
-name = "unicode-bidi"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
-
-[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3760,68 +1182,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "unicode-normalization"
-version = "0.1.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
-
-[[package]]
-name = "universal-hash"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
-dependencies = [
- "crypto-common",
- "subtle",
-]
-
-[[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"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
- "idna 1.1.0",
- "percent-encoding",
- "serde",
- "serde_derive",
-]
-
-[[package]]
-name = "url-fork"
-version = "3.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fa3323c39b8e786154d3000b70ae9af0e9bd746c9791456da0d4a1f68ad89d6"
-dependencies = [
- "form_urlencoded",
- "idna 0.5.0",
+ "idna",
"percent-encoding",
"serde",
]
[[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"
@@ -3834,23 +1206,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
-name = "uuid"
-version = "1.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
-dependencies = [
- "getrandom 0.4.3",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[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"
@@ -3872,30 +1227,6 @@ dependencies = [
]
[[package]]
-name = "want"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
-dependencies = [
- "try-lock",
-]
-
-[[package]]
-name = "wasi"
-version = "0.11.1+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
-
-[[package]]
-name = "wasip2"
-version = "1.0.4+wasi-0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
-dependencies = [
- "wit-bindgen",
-]
-
-[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3910,16 +1241,6 @@ dependencies = [
]
[[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.76"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3952,44 +1273,6 @@ dependencies = [
]
[[package]]
-name = "web-sys"
-version = "0.3.103"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "web-time"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[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.9",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "1.0.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4012,83 +1295,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows-core"
-version = "0.62.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "windows-result"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
-]
-
-[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4098,177 +1310,12 @@ 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 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.53.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
-dependencies = [
- "windows-link",
- "windows_aarch64_gnullvm 0.53.1",
- "windows_aarch64_msvc 0.53.1",
- "windows_i686_gnu 0.53.1",
- "windows_i686_gnullvm 0.53.1",
- "windows_i686_msvc 0.53.1",
- "windows_x86_64_gnu 0.53.1",
- "windows_x86_64_gnullvm 0.53.1",
- "windows_x86_64_msvc 0.53.1",
-]
-
-[[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_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-
-[[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_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
-
-[[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_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-
-[[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_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-
-[[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_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-
-[[package]]
-name = "winnow"
-version = "0.7.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
-[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
-name = "xattr"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
-dependencies = [
- "libc",
- "rustix",
-]
-
-[[package]]
-name = "yaml-rust2"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
-dependencies = [
- "arraydeque",
- "encoding_rs",
- "hashlink 0.8.4",
-]
-
-[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4292,26 +1339,6 @@ dependencies = [
]
[[package]]
-name = "zerocopy"
-version = "0.8.55"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
-dependencies = [
- "zerocopy-derive",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.8.55"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4337,20 +1364,6 @@ name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
-dependencies = [
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
[[package]]
name = "zerotrie"
diff --git a/Cargo.toml b/Cargo.toml
@@ -22,54 +22,13 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[features]
default = []
-test-fixture-geonames-asset = ["radroots_sdk/geonames"]
+test-fixture-geonames-asset = ["radroots/geonames"]
[dependencies]
-chacha20poly1305 = "0.10"
-chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
clap = { version = "4.5", features = ["derive"] }
-getrandom = "0.2"
-nostr = { version = "0.44.2", features = ["nip44"] }
-nostr-sdk = "0.44.1"
-radroots_core = { version = "=0.1.0-alpha", features = ["std", "serde"] }
-radroots_event = "=0.1.0-alpha"
-radroots_event_codec = { version = "=0.1.0-alpha", features = ["json"] }
-radroots_identity = "=0.1.0-alpha"
-radroots_runtime_store = "=0.1.0-alpha"
-radroots_log = "=0.1.0-alpha"
-radroots_mesh = { version = "=0.1.0-alpha", default-features = false }
-radroots_nostr_accounts = { version = "=0.1.0-alpha", features = ["os-keyring"] }
-radroots_nostr = { version = "=0.1.0-alpha", features = ["events", "signing"] }
-radroots_nostr_connect = "=0.1.0-alpha"
-radroots_protected_store = { version = "=0.1.0-alpha", features = ["std"] }
-radroots_replica_store = "=0.1.0-alpha"
-radroots_replica_schema = "=0.1.0-alpha"
-radroots_replica_sync = { version = "=0.1.0-alpha", features = ["legacy-ingest"] }
-radroots_transport = { version = "=0.1.0-alpha", default-features = false }
-radroots_transport_nostr = { version = "=0.1.0-alpha", default-features = false }
-radroots_runtime = "=0.1.0-alpha"
-radroots_runtime_contract_v1 = "=0.1.0-alpha"
-radroots_runtime_paths = "=0.1.0-alpha"
-radroots_sdk = { version = "=0.1.0-alpha", features = ["full"] }
-radroots_secret_vault = { version = "=0.1.0-alpha", features = ["std", "os-keyring"] }
-radroots_signing = { version = "=0.1.0-alpha", features = ["std"] }
-radroots_sql_core = { version = "=0.1.0-alpha", features = ["native"] }
-radroots_storage = "=0.1.0-alpha"
-radroots_storage_sqlite = "=0.1.0-alpha"
-radroots_sync = "=0.1.0-alpha"
-radroots_trade = "=0.1.0-alpha"
-radroots_protocol = "=0.1.0-alpha"
-serde = { version = "1.0", features = ["derive"] }
+radroots = "=0.1.0-alpha"
serde_json = "1.0"
-thiserror = "2.0"
-tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
-toml = "0.8"
-url = "2.5"
-zeroize = "1.8"
+tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[dev-dependencies]
assert_cmd = "2.0"
-flate2 = "1"
-tar = "0.4"
-tempfile = "3.17"
-tungstenite = "0.26.2"
diff --git a/README b/README
@@ -1,10 +1,8 @@
# Radroots CLI
-`radroots` is the local-first command-line client for Radroots accounts, farms,
-listings, markets, baskets, trades, transport, and synchronization. The CLI is
-a downstream consumer of the versioned `radroots_sdk` package and retains only
-host concerns such as configuration, presentation, local process integration,
-and command orchestration.
+`radroots` is the local-first command-line host for the release-v1 Radroots
+crate graph. It consumes the canonical `radroots` facade and retains only
+command parsing and presentation concerns.
The CLI is pre-release software. Its current package graph uses exact alpha
versions so a lockfile and the matching Radroots package set travel together.
@@ -23,20 +21,21 @@ and opt into the ignored local override file described in
[`docs/engineering/local-overrides.md`](docs/engineering/local-overrides.md).
Release and packaged-consumer checks never use sibling path overrides.
-## Command model
+## Release-v1 command model
Commands are organized by resource. Inspect any level with `--help`:
```sh
-radroots health inspect
-radroots account list --format json
-radroots transport status inspect
-radroots store inspect --offline
-radroots sync status
-radroots market product search apples --format ndjson
-radroots listing create --dry-run
+radroots profile inspect --format json
+radroots health inspect --format json
```
+Only local profile and health inspection are executable in the initial crate
+release. The complete resource vocabulary remains parseable so automation gets
+a stable, structured `unsupported_operation` response while private predecessor
+runtimes are retired. No command silently falls back to a sibling checkout or
+legacy engine.
+
Global controls include:
- `--format terminal|json|ndjson` for stable output envelopes;
@@ -67,15 +66,16 @@ RADROOTS_CLI_OUTPUT_FORMAT=terminal
RADROOTS_CLI_ACCOUNT_SECRET_BACKEND=encrypted_file
```
-Use `radroots profile inspect` and `radroots health inspect` to see resolved
-paths and actionable readiness state before attempting a write. Account secrets
-are never accepted as command-line values; import them from a protected file or
-use the configured host secret backend.
+Use `radroots profile inspect` and `radroots health inspect` to verify that the
+registry-backed facade can construct and close its local-only memory client.
+Write operations remain fail-closed until their canonical SDK orchestration is
+enabled in a future release.
## Package migration
-The v1 crate migration replaced compatibility namespaces and CLI-owned generic
-signing, transport, storage, and sync engines with the final SDK composition.
+The v1 crate migration removed compatibility namespaces and the CLI-owned
+generic signing, transport, storage, and sync engines. The initial CLI host
+uses only the final facade composition.
See [`docs/migration/crates-release-v1.md`](docs/migration/crates-release-v1.md)
for renamed commands, configuration changes, and downstream package rules.
diff --git a/docs/migration/crates-release-v1.md b/docs/migration/crates-release-v1.md
@@ -6,7 +6,7 @@ engines, and implicit workspace paths are no longer supported.
## Dependency migration
-- Depend on `radroots_sdk = "=0.1.0-alpha"` for the supported application API.
+- Depend on `radroots = "=0.1.0-alpha"` for the supported ordinary application API.
- Keep exact versions and a committed lockfile during the coordinated alpha.
- Do not add sibling `path` dependencies to a production manifest.
- Local contributors may use the explicit ignored patch file documented in
@@ -19,11 +19,11 @@ or merge those repositories.
## Runtime migration
-The CLI now composes the canonical SDK and shared crates for event codecs,
-signing, Nostr transport, storage, and sync. CLI code continues to own command
-parsing, configuration precedence, terminal and structured presentation,
-process integration, and local host policy. It must not reintroduce a generic
-relay pagination loop, ingest reducer, outbox engine, or signer protocol.
+The CLI now composes only the canonical `radroots` facade. CLI code owns
+command parsing and terminal or structured presentation; storage, signing,
+transport, and sync behavior remain with the final crates. It must not
+reintroduce a generic relay pagination loop, ingest reducer, outbox engine, or
+signer protocol.
The old environment and TOML groups are rejected instead of silently mapped.
Start from `.env.example`, inspect the resolved profile, and address health
@@ -32,11 +32,12 @@ actions in order:
```sh
radroots profile inspect --format json
radroots health inspect --format json
-radroots signer status --format json
-radroots transport status inspect --format json
-radroots sync status --format json
```
+Other resource commands remain parseable but fail closed with
+`unsupported_operation` until a future release enables their canonical SDK
+orchestration. This is an intentional breaking change from the private runtime.
+
Automation should use `--no-input`, explicit online/offline policy, and stable
idempotency and correlation identifiers for writes. Retired commands and flags
fail during parsing; use the resource-oriented command tree shown by
diff --git a/src/cli/global.rs b/src/cli/global.rs
@@ -1,219 +0,0 @@
-use std::path::PathBuf;
-
-use crate::runtime::config::OutputFormat;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RuntimeOutputFormatArg {
- Terminal,
- Json,
- Ndjson,
-}
-
-impl RuntimeOutputFormatArg {
- pub fn as_output_format(self) -> OutputFormat {
- match self {
- Self::Terminal => OutputFormat::Terminal,
- Self::Json => OutputFormat::Json,
- Self::Ndjson => OutputFormat::Ndjson,
- }
- }
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct RuntimeInvocationArgs {
- pub output_format: Option<RuntimeOutputFormatArg>,
- pub json: bool,
- pub ndjson: bool,
- pub env_file: Option<PathBuf>,
- pub quiet: bool,
- pub verbose: bool,
- pub trace: bool,
- pub dry_run: bool,
- pub no_input: bool,
- pub yes: bool,
- pub log_filter: Option<String>,
- pub log_dir: Option<PathBuf>,
- pub log_stdout: bool,
- pub no_log_stdout: bool,
- pub account: Option<String>,
- pub identity_path: Option<PathBuf>,
- pub signer: Option<String>,
- pub myc_executable: Option<PathBuf>,
- pub myc_status_timeout_ms: Option<u64>,
- pub hyf_enabled: bool,
- pub no_hyf_enabled: bool,
- pub hyf_executable: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum LocalExportFormatArg {
- Json,
- Ndjson,
-}
-
-impl LocalExportFormatArg {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Json => "json",
- Self::Ndjson => "ndjson",
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct SyncWatchArgs {
- pub frames: usize,
- pub interval_ms: u64,
-}
-
-#[derive(Debug, Clone)]
-pub struct FindQueryArgs {
- pub query: Vec<String>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FarmScopeArg {
- User,
- Workspace,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FarmFieldArg {
- Name,
- DisplayName,
- About,
- Website,
- Picture,
- Banner,
- Location,
- City,
- Region,
- Country,
- Geohash,
- Delivery,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FarmScopedArgs {
- pub scope: Option<FarmScopeArg>,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FarmCreateArgs {
- pub scope: Option<FarmScopeArg>,
- pub farm_d_tag: Option<String>,
- pub name: Option<String>,
- pub display_name: Option<String>,
- pub about: Option<String>,
- pub website: Option<String>,
- pub picture: Option<String>,
- pub banner: Option<String>,
- pub location: Option<String>,
- pub city: Option<String>,
- pub region: Option<String>,
- pub country: Option<String>,
- pub geohash: Option<String>,
- pub delivery_method: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct FarmRebindArgs {
- pub scope: Option<FarmScopeArg>,
- pub selector: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct FarmUpdateArgs {
- pub scope: Option<FarmScopeArg>,
- pub field: FarmFieldArg,
- pub value: Vec<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct FarmPrivateLocationSetArgs {
- pub farm_d_tag: Option<String>,
- pub input: FarmPrivateLocationSetInput,
- pub label: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub enum FarmPrivateLocationSetInput {
- Exact {
- latitude: f64,
- longitude: f64,
- },
- City {
- city: String,
- region: Option<String>,
- country: Option<String>,
- },
- Query(String),
- GeonamesId(i64),
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FarmPrivateLocationKeyArgs {
- pub farm_d_tag: Option<String>,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FarmPublishArgs {
- pub scope: Option<FarmScopeArg>,
- pub idempotency_key: Option<String>,
- pub print_event: bool,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct ListingCreateArgs {
- pub output: Option<PathBuf>,
- pub key: Option<String>,
- pub title: Option<String>,
- pub category: Option<String>,
- pub summary: Option<String>,
- pub bin_id: Option<String>,
- pub quantity_amount: Option<String>,
- pub quantity_unit: Option<String>,
- pub price_amount: Option<String>,
- pub price_currency: Option<String>,
- pub price_per_amount: Option<String>,
- pub price_per_unit: Option<String>,
- pub available: Option<String>,
- pub label: Option<String>,
- pub discount_id: Option<String>,
- pub discount_label: Option<String>,
- pub discount_kind: Option<String>,
- pub discount_value: Option<String>,
- pub discount_amount: Option<String>,
- pub discount_currency: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct ListingFileArgs {
- pub file: PathBuf,
-}
-
-#[derive(Debug, Clone)]
-pub struct ListingAppRecordExportArgs {
- pub record_id: String,
- pub output: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone)]
-pub struct ListingRebindArgs {
- pub file: PathBuf,
- pub selector: String,
- pub farm_d_tag: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct ListingMutationArgs {
- pub file: PathBuf,
- pub idempotency_key: Option<String>,
- pub print_event: bool,
- pub offline: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct RecordLookupArgs {
- pub key: String,
-}
diff --git a/src/cli/input.rs b/src/cli/input.rs
@@ -1,340 +0,0 @@
-use std::path::Path;
-
-use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
-use crate::cli::{TargetCliArgs, TargetCommand, TargetOutputFormat};
-use crate::ops::OperationData;
-use serde_json::Value;
-
-pub fn runtime_invocation_args_from_target(args: &TargetCliArgs) -> RuntimeInvocationArgs {
- RuntimeInvocationArgs {
- output_format: args.format.map(|format| match format {
- TargetOutputFormat::Terminal => RuntimeOutputFormatArg::Terminal,
- TargetOutputFormat::Json => RuntimeOutputFormatArg::Json,
- TargetOutputFormat::Ndjson => RuntimeOutputFormatArg::Ndjson,
- }),
- json: false,
- ndjson: false,
- env_file: None,
- quiet: args.quiet,
- verbose: args.verbose,
- trace: args.trace,
- dry_run: args.dry_run,
- no_input: args.no_input,
- yes: args.yes,
- log_filter: None,
- log_dir: None,
- log_stdout: false,
- no_log_stdout: false,
- account: args.account_id.clone(),
- identity_path: None,
- signer: None,
- myc_executable: None,
- myc_status_timeout_ms: None,
- hyf_enabled: false,
- no_hyf_enabled: false,
- hyf_executable: None,
- }
-}
-
-pub fn operation_id_from_target(args: &TargetCliArgs) -> &'static str {
- args.command.operation_id()
-}
-
-pub fn target_operation_input(command: &TargetCommand) -> OperationData {
- use crate::cli::{
- AccountCommand, BasketCommand, BasketItemCommand, FarmCommand, ListingCommand,
- MarketCommand, StoreCommand, TradeCancellationCommand, TradeCandidateCommand, TradeCommand,
- TradeEvidenceCommand, TradeOperationCommand, TradePrivateArtifactCommand,
- TradeProposalCommand, TradeRevisionCommand, TransportCapabilityCommand, TransportCommand,
- TransportConfigCommand, TransportDeliveryCommand, ValidationCommand,
- };
-
- let mut input = OperationData::new();
- match command {
- TargetCommand::Account(args) => match &args.command {
- AccountCommand::Import(args) => {
- insert_path(&mut input, "path", &args.path);
- if args.default {
- input.insert("default".to_owned(), Value::Bool(true));
- }
- }
- AccountCommand::Select(args) => insert_string(&mut input, "selector", &args.selector),
- AccountCommand::Remove(args) => insert_string(&mut input, "selector", &args.selector),
- AccountCommand::Create | AccountCommand::List => {}
- },
- TargetCommand::Farm(args) => match &args.command {
- FarmCommand::Create(args) => {
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- insert_string(&mut input, "name", &args.name);
- insert_string(&mut input, "display_name", &args.display_name);
- insert_string(&mut input, "about", &args.about);
- insert_string(&mut input, "website", &args.website);
- insert_string(&mut input, "picture", &args.picture);
- insert_string(&mut input, "banner", &args.banner);
- insert_string(&mut input, "location", &args.location);
- insert_string(&mut input, "city", &args.city);
- insert_string(&mut input, "region", &args.region);
- insert_string(&mut input, "country", &args.country);
- insert_string(&mut input, "geohash", &args.geohash);
- insert_string(&mut input, "delivery_method", &args.delivery_method);
- }
- FarmCommand::Update(args) => {
- insert_string(&mut input, "field", &args.field);
- insert_string(&mut input, "value", &args.value);
- }
- FarmCommand::Get | FarmCommand::List | FarmCommand::Publish => {}
- },
- TargetCommand::Listing(args) => match &args.command {
- ListingCommand::Create(args) => {
- insert_path(&mut input, "output", &args.output);
- insert_string(&mut input, "key", &args.key);
- insert_string(&mut input, "title", &args.title);
- insert_string(&mut input, "category", &args.category);
- insert_string(&mut input, "summary", &args.summary);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity_amount", &args.quantity_amount);
- insert_string(&mut input, "quantity_unit", &args.quantity_unit);
- insert_string(&mut input, "price_amount", &args.price_amount);
- insert_string(&mut input, "price_currency", &args.price_currency);
- insert_string(&mut input, "price_per_amount", &args.price_per_amount);
- insert_string(&mut input, "price_per_unit", &args.price_per_unit);
- insert_string(&mut input, "available", &args.available);
- insert_string(&mut input, "label", &args.label);
- insert_string(&mut input, "discount_id", &args.discount_id);
- insert_string(&mut input, "discount_label", &args.discount_label);
- insert_string(&mut input, "discount_kind", &args.discount_kind);
- insert_string(&mut input, "discount_value", &args.discount_value);
- insert_string(&mut input, "discount_amount", &args.discount_amount);
- insert_string(&mut input, "discount_currency", &args.discount_currency);
- }
- ListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- ListingCommand::Update(args)
- | ListingCommand::Publish(args)
- | ListingCommand::Pause(args)
- | ListingCommand::Withdraw(args) => insert_path(&mut input, "file", &args.file),
- ListingCommand::List => {}
- },
- TargetCommand::Market(args) => match &args.command {
- MarketCommand::Pull => {}
- MarketCommand::Search(args) => {
- insert_string_array(&mut input, "query", args.query.as_slice())
- }
- MarketCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- },
- TargetCommand::Store(args) => match &args.command {
- StoreCommand::Restore(args) => {
- insert_path(&mut input, "source", &Some(args.source.clone()));
- insert_path(&mut input, "destination", &args.destination);
- if args.overwrite {
- input.insert("overwrite".to_owned(), Value::Bool(true));
- }
- }
- StoreCommand::Inspect | StoreCommand::Backup => {}
- },
- TargetCommand::Basket(args) => match &args.command {
- BasketCommand::Create(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "listing", &args.listing);
- insert_string(&mut input, "listing_addr", &args.listing_addr);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity", &args.quantity);
- }
- BasketCommand::Get(args) => insert_string(&mut input, "basket_id", &args.basket_id),
- BasketCommand::Item(item) => match &item.command {
- BasketItemCommand::Add(args) | BasketItemCommand::Update(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "item_id", &args.item_id);
- insert_string(&mut input, "listing", &args.listing);
- insert_string(&mut input, "listing_addr", &args.listing_addr);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity", &args.quantity);
- }
- BasketItemCommand::Remove(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "item_id", &args.item_id);
- }
- },
- BasketCommand::Quote(args) => insert_string(&mut input, "basket_id", &args.basket_id),
- BasketCommand::List => {}
- },
- TargetCommand::Trade(args) => match &args.command {
- TradeCommand::Proposal(args) => match &args.command {
- TradeProposalCommand::Submit(args) => {
- insert_path_required(&mut input, "file", &args.file)
- }
- },
- TradeCommand::Revision(args) => match &args.command {
- TradeRevisionCommand::Propose(args) => {
- insert_path_required(&mut input, "file", &args.file)
- }
- },
- TradeCommand::Candidate(args) => match &args.command {
- TradeCandidateCommand::Decide(args) => {
- insert_path_required(&mut input, "file", &args.file);
- insert_bool(
- &mut input,
- "acknowledge_private_terms",
- args.acknowledge_private_terms,
- );
- }
- },
- TradeCommand::Cancellation(args) => match &args.command {
- TradeCancellationCommand::Submit(args) => {
- insert_path_required(&mut input, "file", &args.file)
- }
- },
- TradeCommand::Operation(args) => match &args.command {
- TradeOperationCommand::Resume(args) => {
- insert_path_required(&mut input, "file", &args.file);
- if let Some(operation_kind) = args.operation_kind {
- input.insert(
- "operation_kind".to_owned(),
- Value::String(operation_kind.as_sdk_operation_kind().to_owned()),
- );
- }
- insert_bool(
- &mut input,
- "acknowledge_private_terms",
- args.acknowledge_private_terms,
- );
- }
- },
- TradeCommand::Get(args) => insert_string(&mut input, "trade_id", &args.trade_id),
- TradeCommand::List(args) => {
- insert_u32(&mut input, "limit", args.limit);
- insert_string(&mut input, "cursor", &args.cursor);
- }
- TradeCommand::Evidence(args) => match &args.command {
- TradeEvidenceCommand::Refresh(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id)
- }
- TradeEvidenceCommand::Inspect(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id);
- insert_u32(&mut input, "limit", args.limit);
- insert_string(&mut input, "cursor", &args.cursor);
- }
- },
- TradeCommand::PrivateArtifact(args) => match &args.command {
- TradePrivateArtifactCommand::Seal(args) => {
- insert_string(&mut input, "trade_id", &args.trade_id);
- insert_string(&mut input, "artifact_id", &args.artifact_id);
- insert_string(&mut input, "schema_id", &args.schema_id);
- insert_path(&mut input, "input", &args.input);
- input.insert(
- "kind".to_owned(),
- Value::String(args.kind.as_str().to_owned()),
- );
- insert_string(&mut input, "candidate_id", &args.candidate_id);
- insert_string(&mut input, "retention_class", &args.retention_class);
- insert_i64(&mut input, "expires_at_ms", args.expires_at_ms);
- }
- TradePrivateArtifactCommand::Open(args) => {
- input.insert(
- "artifact_id".to_owned(),
- Value::String(args.artifact_id.clone()),
- );
- insert_path(&mut input, "output", &args.output);
- }
- TradePrivateArtifactCommand::Delete(args) => {
- input.insert(
- "artifact_id".to_owned(),
- Value::String(args.artifact_id.clone()),
- );
- }
- },
- },
- TargetCommand::Validation(args) => match &args.command {
- ValidationCommand::Status => {}
- },
- TargetCommand::Transport(args) => match &args.command {
- TransportCommand::Config(config) => match &config.command {
- TransportConfigCommand::Update(args) => {
- input.insert(
- "kind".to_owned(),
- Value::String(args.kind.as_str().to_owned()),
- );
- insert_string_array(&mut input, "nostr_relays", args.nostr_relay.as_slice());
- if let Some(behavior) = args.reticulum_behavior {
- input.insert(
- "reticulum_behavior".to_owned(),
- Value::String(behavior.as_str().to_owned()),
- );
- }
- insert_string(&mut input, "reticulum_scope", &args.reticulum_scope);
- insert_string(
- &mut input,
- "reticulum_agent_endpoint",
- &args.reticulum_agent_endpoint,
- );
- }
- TransportConfigCommand::Inspect => {}
- },
- TransportCommand::Capability(capability) => match capability.command {
- TransportCapabilityCommand::List => {}
- },
- TransportCommand::Delivery(delivery) => match delivery.command {
- TransportDeliveryCommand::Inspect | TransportDeliveryCommand::Retry => {}
- },
- TransportCommand::Status(_) => {}
- },
- _ => {}
- }
- input
-}
-
-fn insert_string(input: &mut OperationData, key: &str, value: &Option<String>) {
- if let Some(value) = value
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- input.insert(key.to_owned(), Value::String(value.to_owned()));
- }
-}
-
-fn insert_string_array(input: &mut OperationData, key: &str, values: &[String]) {
- let values = values
- .iter()
- .map(String::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(|value| Value::String(value.to_owned()))
- .collect::<Vec<_>>();
- if !values.is_empty() {
- input.insert(key.to_owned(), Value::Array(values));
- }
-}
-
-fn insert_bool(input: &mut OperationData, key: &str, value: bool) {
- if value {
- input.insert(key.to_owned(), Value::Bool(true));
- }
-}
-
-fn insert_path(input: &mut OperationData, key: &str, value: &Option<std::path::PathBuf>) {
- if let Some(value) = value {
- input.insert(
- key.to_owned(),
- Value::String(value.to_string_lossy().into_owned()),
- );
- }
-}
-
-fn insert_path_required(input: &mut OperationData, key: &str, value: &Path) {
- input.insert(
- key.to_owned(),
- Value::String(value.to_string_lossy().into_owned()),
- );
-}
-
-fn insert_u32(input: &mut OperationData, key: &str, value: Option<u32>) {
- if let Some(value) = value {
- input.insert(key.to_owned(), Value::Number(value.into()));
- }
-}
-
-fn insert_i64(input: &mut OperationData, key: &str, value: Option<i64>) {
- if let Some(value) = value {
- input.insert(key.to_owned(), Value::Number(value.into()));
- }
-}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
@@ -1,11 +1,8 @@
-pub mod global;
-
pub mod account;
pub mod basket;
pub mod diagnostics;
pub mod farm;
pub mod health;
-pub mod input;
pub mod listing;
pub mod market;
pub mod profile;
@@ -254,19 +251,6 @@ mod tests {
use clap::{CommandFactory, Parser};
use super::{TargetCliArgs, TargetCommand, TargetOutputFormat};
- use crate::registry::OPERATION_REGISTRY;
-
- #[test]
- fn target_parser_accepts_every_generated_registry_path() {
- for operation in OPERATION_REGISTRY {
- let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
- .unwrap_or_else(|error| {
- panic!("{} failed to parse: {error}", operation.cli_path);
- });
- assert_eq!(parsed.command.operation_id(), operation.operation_id);
- }
- }
-
#[test]
fn target_parser_exposes_only_v1_namespaces() {
let actual = TargetCliArgs::command()
diff --git a/src/generated/runtime_contract_registry.rs b/src/generated/runtime_contract_registry.rs
@@ -1,859 +0,0 @@
-// @generated by radroots_sdk_xtask. Do not edit by hand.
-
-pub const OPERATION_REGISTRY: &[OperationSpec] = &[
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ProfileInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[0],
- operation_id: "profile.inspect",
- cli_path: "radroots profile inspect",
- namespace: "profile",
- mcp_tool: "profile_inspect",
- rust_request: "ProfileInspectRequest",
- rust_result: "ProfileInspectResult",
- json_kind: "profile.inspect",
- description: "Inspect the active runtime profile.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ProfileReset,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[1],
- operation_id: "profile.reset",
- cli_path: "radroots profile reset",
- namespace: "profile",
- mcp_tool: "profile_reset",
- rust_request: "ProfileResetRequest",
- rust_result: "ProfileResetResult",
- json_kind: "profile.reset",
- description: "Reset the active runtime profile stores.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::AccountCreate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[2],
- operation_id: "account.create",
- cli_path: "radroots account create",
- namespace: "account",
- mcp_tool: "account_create",
- rust_request: "AccountCreateRequest",
- rust_result: "AccountCreateResult",
- json_kind: "account.create",
- description: "Create a local account identity.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::AccountImport,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[3],
- operation_id: "account.import",
- cli_path: "radroots account import",
- namespace: "account",
- mcp_tool: "account_import",
- rust_request: "AccountImportRequest",
- rust_result: "AccountImportResult",
- json_kind: "account.import",
- description: "Import an existing account identity.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::AccountSelect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[4],
- operation_id: "account.select",
- cli_path: "radroots account select <selector>",
- namespace: "account",
- mcp_tool: "account_select",
- rust_request: "AccountSelectRequest",
- rust_result: "AccountSelectResult",
- json_kind: "account.select",
- description: "Select the active account context.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::AccountList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[5],
- operation_id: "account.list",
- cli_path: "radroots account list",
- namespace: "account",
- mcp_tool: "account_list",
- rust_request: "AccountListRequest",
- rust_result: "AccountListResult",
- json_kind: "account.list",
- description: "List known local accounts.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::AccountRemove,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[6],
- operation_id: "account.remove",
- cli_path: "radroots account remove <selector>",
- namespace: "account",
- mcp_tool: "account_remove",
- rust_request: "AccountRemoveRequest",
- rust_result: "AccountRemoveResult",
- json_kind: "account.remove",
- description: "Remove an account from local configuration and storage.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::SignerStatus,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[7],
- operation_id: "signer.status",
- cli_path: "radroots signer status",
- namespace: "signer",
- mcp_tool: "signer_status",
- rust_request: "SignerStatusRequest",
- rust_result: "SignerStatusResult",
- json_kind: "signer.status",
- description: "Inspect signer readiness.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::StoreInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[8],
- operation_id: "store.inspect",
- cli_path: "radroots store inspect",
- namespace: "store",
- mcp_tool: "store_inspect",
- rust_request: "StoreInspectRequest",
- rust_result: "StoreInspectResult",
- json_kind: "store.inspect",
- description: "Inspect local runtime stores.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::StoreBackup,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[9],
- operation_id: "store.backup",
- cli_path: "radroots store backup",
- namespace: "store",
- mcp_tool: "store_backup",
- rust_request: "StoreBackupRequest",
- rust_result: "StoreBackupResult",
- json_kind: "store.backup",
- description: "Create a local runtime-store backup.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::StoreRestore,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[10],
- operation_id: "store.restore",
- cli_path: "radroots store restore <source>",
- namespace: "store",
- mcp_tool: "store_restore",
- rust_request: "StoreRestoreRequest",
- rust_result: "StoreRestoreResult",
- json_kind: "store.restore",
- description: "Restore local runtime stores from a backup.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::FarmCreate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[11],
- operation_id: "farm.create",
- cli_path: "radroots farm create",
- namespace: "farm",
- mcp_tool: "farm_create",
- rust_request: "FarmCreateRequest",
- rust_result: "FarmCreateResult",
- json_kind: "farm.create",
- description: "Create farm profile data.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::FarmUpdate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[12],
- operation_id: "farm.update",
- cli_path: "radroots farm update",
- namespace: "farm",
- mcp_tool: "farm_update",
- rust_request: "FarmUpdateRequest",
- rust_result: "FarmUpdateResult",
- json_kind: "farm.update",
- description: "Update farm profile data.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::FarmPublish,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[13],
- operation_id: "farm.publish",
- cli_path: "radroots farm publish",
- namespace: "farm",
- mcp_tool: "farm_publish",
- rust_request: "FarmPublishRequest",
- rust_result: "FarmPublishResult",
- json_kind: "farm.publish",
- description: "Publish farm profile data.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::FarmGet,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[14],
- operation_id: "farm.get",
- cli_path: "radroots farm get",
- namespace: "farm",
- mcp_tool: "farm_get",
- rust_request: "FarmGetRequest",
- rust_result: "FarmGetResult",
- json_kind: "farm.get",
- description: "Get farm profile data.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::FarmList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[15],
- operation_id: "farm.list",
- cli_path: "radroots farm list",
- namespace: "farm",
- mcp_tool: "farm_list",
- rust_request: "FarmListRequest",
- rust_result: "FarmListResult",
- json_kind: "farm.list",
- description: "List farm profile data.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingCreate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[16],
- operation_id: "listing.create",
- cli_path: "radroots listing create",
- namespace: "listing",
- mcp_tool: "listing_create",
- rust_request: "ListingCreateRequest",
- rust_result: "ListingCreateResult",
- json_kind: "listing.create",
- description: "Create a listing draft.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingUpdate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[17],
- operation_id: "listing.update",
- cli_path: "radroots listing update <file>",
- namespace: "listing",
- mcp_tool: "listing_update",
- rust_request: "ListingUpdateRequest",
- rust_result: "ListingUpdateResult",
- json_kind: "listing.update",
- description: "Update a listing.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingPublish,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[18],
- operation_id: "listing.publish",
- cli_path: "radroots listing publish <file>",
- namespace: "listing",
- mcp_tool: "listing_publish",
- rust_request: "ListingPublishRequest",
- rust_result: "ListingPublishResult",
- json_kind: "listing.publish",
- description: "Publish a listing.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingPause,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[19],
- operation_id: "listing.pause",
- cli_path: "radroots listing pause <file>",
- namespace: "listing",
- mcp_tool: "listing_pause",
- rust_request: "ListingPauseRequest",
- rust_result: "ListingPauseResult",
- json_kind: "listing.pause",
- description: "Pause a listing.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingWithdraw,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[20],
- operation_id: "listing.withdraw",
- cli_path: "radroots listing withdraw <file>",
- namespace: "listing",
- mcp_tool: "listing_withdraw",
- rust_request: "ListingWithdrawRequest",
- rust_result: "ListingWithdrawResult",
- json_kind: "listing.withdraw",
- description: "Withdraw a listing.",
- role: OperationRole::Seller,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingGet,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[21],
- operation_id: "listing.get",
- cli_path: "radroots listing get <key>",
- namespace: "listing",
- mcp_tool: "listing_get",
- rust_request: "ListingGetRequest",
- rust_result: "ListingGetResult",
- json_kind: "listing.get",
- description: "Get a listing.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ListingList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[22],
- operation_id: "listing.list",
- cli_path: "radroots listing list",
- namespace: "listing",
- mcp_tool: "listing_list",
- rust_request: "ListingListRequest",
- rust_result: "ListingListResult",
- json_kind: "listing.list",
- description: "List listings.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::MarketPull,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[23],
- operation_id: "market.pull",
- cli_path: "radroots market pull",
- namespace: "market",
- mcp_tool: "market_pull",
- rust_request: "MarketPullRequest",
- rust_result: "MarketPullResult",
- json_kind: "market.pull",
- description: "Pull market data into the local projection.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::MarketSearch,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[24],
- operation_id: "market.search",
- cli_path: "radroots market search <query>",
- namespace: "market",
- mcp_tool: "market_search",
- rust_request: "MarketSearchRequest",
- rust_result: "MarketSearchResult",
- json_kind: "market.search",
- description: "Search local market projections.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::MarketGet,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[25],
- operation_id: "market.get",
- cli_path: "radroots market get <key>",
- namespace: "market",
- mcp_tool: "market_get",
- rust_request: "MarketGetRequest",
- rust_result: "MarketGetResult",
- json_kind: "market.get",
- description: "Get market listing details.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketCreate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[26],
- operation_id: "basket.create",
- cli_path: "radroots basket create",
- namespace: "basket",
- mcp_tool: "basket_create",
- rust_request: "BasketCreateRequest",
- rust_result: "BasketCreateResult",
- json_kind: "basket.create",
- description: "Create a basket.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketGet,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[27],
- operation_id: "basket.get",
- cli_path: "radroots basket get <basket-id>",
- namespace: "basket",
- mcp_tool: "basket_get",
- rust_request: "BasketGetRequest",
- rust_result: "BasketGetResult",
- json_kind: "basket.get",
- description: "Get a basket.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[28],
- operation_id: "basket.list",
- cli_path: "radroots basket list",
- namespace: "basket",
- mcp_tool: "basket_list",
- rust_request: "BasketListRequest",
- rust_result: "BasketListResult",
- json_kind: "basket.list",
- description: "List baskets.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketItemAdd,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[29],
- operation_id: "basket.item.add",
- cli_path: "radroots basket item add <basket-id>",
- namespace: "basket",
- mcp_tool: "basket_item_add",
- rust_request: "BasketItemAddRequest",
- rust_result: "BasketItemAddResult",
- json_kind: "basket.item.add",
- description: "Add an item to a basket.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketItemUpdate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[30],
- operation_id: "basket.item.update",
- cli_path: "radroots basket item update <basket-id>",
- namespace: "basket",
- mcp_tool: "basket_item_update",
- rust_request: "BasketItemUpdateRequest",
- rust_result: "BasketItemUpdateResult",
- json_kind: "basket.item.update",
- description: "Update a basket item.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketItemRemove,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[31],
- operation_id: "basket.item.remove",
- cli_path: "radroots basket item remove <basket-id>",
- namespace: "basket",
- mcp_tool: "basket_item_remove",
- rust_request: "BasketItemRemoveRequest",
- rust_result: "BasketItemRemoveResult",
- json_kind: "basket.item.remove",
- description: "Remove a basket item.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::BasketQuote,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[32],
- operation_id: "basket.quote",
- cli_path: "radroots basket quote <basket-id>",
- namespace: "basket",
- mcp_tool: "basket_quote",
- rust_request: "BasketQuoteRequest",
- rust_result: "BasketQuoteResult",
- json_kind: "basket.quote",
- description: "Quote a basket.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeProposalSubmit,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[33],
- operation_id: "trade.proposal.submit",
- cli_path: "radroots trade proposal submit <file>",
- namespace: "trade",
- mcp_tool: "trade_proposal_submit",
- rust_request: "TradeProposalSubmitRequest",
- rust_result: "TradeProposalSubmitResult",
- json_kind: "trade.proposal.submit",
- description: "Submit a trade proposal from a complete candidate draft.",
- role: OperationRole::Buyer,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeRevisionPropose,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[34],
- operation_id: "trade.revision.propose",
- cli_path: "radroots trade revision propose <file>",
- namespace: "trade",
- mcp_tool: "trade_revision_propose",
- rust_request: "TradeRevisionProposeRequest",
- rust_result: "TradeRevisionProposeResult",
- json_kind: "trade.revision.propose",
- description: "Propose a complete replacement candidate for a trade.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeCandidateDecide,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[35],
- operation_id: "trade.candidate.decide",
- cli_path: "radroots trade candidate decide <file>",
- namespace: "trade",
- mcp_tool: "trade_candidate_decide",
- rust_request: "TradeCandidateDecideRequest",
- rust_result: "TradeCandidateDecideResult",
- json_kind: "trade.candidate.decide",
- description: "Decide a candidate proposal.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeCancellationSubmit,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[36],
- operation_id: "trade.cancellation.submit",
- cli_path: "radroots trade cancellation submit <file>",
- namespace: "trade",
- mcp_tool: "trade_cancellation_submit",
- rust_request: "TradeCancellationSubmitRequest",
- rust_result: "TradeCancellationSubmitResult",
- json_kind: "trade.cancellation.submit",
- description: "Submit a trade cancellation mutation.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeOperationResume,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[37],
- operation_id: "trade.operation.resume",
- cli_path: "radroots trade operation resume <file>",
- namespace: "trade",
- mcp_tool: "trade_operation_resume",
- rust_request: "TradeOperationResumeRequest",
- rust_result: "TradeOperationResumeResult",
- json_kind: "trade.operation.resume",
- description: "Resume a prepared trade operation envelope.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeGet,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[38],
- operation_id: "trade.get",
- cli_path: "radroots trade get <key>",
- namespace: "trade",
- mcp_tool: "trade_get",
- rust_request: "TradeGetRequest",
- rust_result: "TradeGetResult",
- json_kind: "trade.get",
- description: "Get SDK trade projection details.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[39],
- operation_id: "trade.list",
- cli_path: "radroots trade list",
- namespace: "trade",
- mcp_tool: "trade_list",
- rust_request: "TradeListRequest",
- rust_result: "TradeListResult",
- json_kind: "trade.list",
- description: "List SDK trade projections.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeEvidenceRefresh,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[40],
- operation_id: "trade.evidence.refresh",
- cli_path: "radroots trade evidence refresh <trade-id>",
- namespace: "trade",
- mcp_tool: "trade_evidence_refresh",
- rust_request: "TradeEvidenceRefreshRequest",
- rust_result: "TradeEvidenceRefreshResult",
- json_kind: "trade.evidence.refresh",
- description: "Refresh SDK trade evidence projections.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradeEvidenceInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[41],
- operation_id: "trade.evidence.inspect",
- cli_path: "radroots trade evidence inspect <trade-id>",
- namespace: "trade",
- mcp_tool: "trade_evidence_inspect",
- rust_request: "TradeEvidenceInspectRequest",
- rust_result: "TradeEvidenceInspectResult",
- json_kind: "trade.evidence.inspect",
- description: "Inspect protected trade evidence metadata.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradePrivateArtifactSeal,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[42],
- operation_id: "trade.private_artifact.seal",
- cli_path: "radroots trade private-artifact seal <trade-id>",
- namespace: "trade",
- mcp_tool: "trade_private_artifact_seal",
- rust_request: "TradePrivateArtifactSealRequest",
- rust_result: "TradePrivateArtifactSealResult",
- json_kind: "trade.private_artifact.seal",
- description: "Seal a protected private trade artifact.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradePrivateArtifactOpen,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[43],
- operation_id: "trade.private_artifact.open",
- cli_path: "radroots trade private-artifact open <artifact-id>",
- namespace: "trade",
- mcp_tool: "trade_private_artifact_open",
- rust_request: "TradePrivateArtifactOpenRequest",
- rust_result: "TradePrivateArtifactOpenResult",
- json_kind: "trade.private_artifact.open",
- description: "Open a protected private trade artifact.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TradePrivateArtifactDelete,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[44],
- operation_id: "trade.private_artifact.delete",
- cli_path: "radroots trade private-artifact delete <artifact-id>",
- namespace: "trade",
- mcp_tool: "trade_private_artifact_delete",
- rust_request: "TradePrivateArtifactDeleteRequest",
- rust_result: "TradePrivateArtifactDeleteResult",
- json_kind: "trade.private_artifact.delete",
- description: "Delete a protected private trade artifact.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::ValidationStatus,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[45],
- operation_id: "validation.status",
- cli_path: "radroots validation status",
- namespace: "validation",
- mcp_tool: "validation_status",
- rust_request: "ValidationStatusRequest",
- rust_result: "ValidationStatusResult",
- json_kind: "validation.status",
- description: "Inspect validation status.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::SyncStatus,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[46],
- operation_id: "sync.status",
- cli_path: "radroots sync status",
- namespace: "sync",
- mcp_tool: "sync_status",
- rust_request: "SyncStatusRequest",
- rust_result: "SyncStatusResult",
- json_kind: "sync.status",
- description: "Inspect synchronization status.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::SyncPull,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[47],
- operation_id: "sync.pull",
- cli_path: "radroots sync pull",
- namespace: "sync",
- mcp_tool: "sync_pull",
- rust_request: "SyncPullRequest",
- rust_result: "SyncPullResult",
- json_kind: "sync.pull",
- description: "Pull synchronization events.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::SyncPush,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[48],
- operation_id: "sync.push",
- cli_path: "radroots sync push",
- namespace: "sync",
- mcp_tool: "sync_push",
- rust_request: "SyncPushRequest",
- rust_result: "SyncPushResult",
- json_kind: "sync.push",
- description: "Push queued synchronization events.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::HealthInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[49],
- operation_id: "health.inspect",
- cli_path: "radroots health inspect",
- namespace: "health",
- mcp_tool: "health_inspect",
- rust_request: "HealthInspectRequest",
- rust_result: "HealthInspectResult",
- json_kind: "health.inspect",
- description: "Inspect runtime health.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportCapabilityList,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[50],
- operation_id: "transport.capability.list",
- cli_path: "radroots transport capability list",
- namespace: "transport",
- mcp_tool: "transport_capability_list",
- rust_request: "TransportCapabilityListRequest",
- rust_result: "TransportCapabilityListResult",
- json_kind: "transport.capability.list",
- description: "List transport capabilities.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportConfigInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[51],
- operation_id: "transport.config.inspect",
- cli_path: "radroots transport config inspect",
- namespace: "transport",
- mcp_tool: "transport_config_inspect",
- rust_request: "TransportConfigInspectRequest",
- rust_result: "TransportConfigInspectResult",
- json_kind: "transport.config.inspect",
- description: "Inspect transport configuration.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportConfigUpdate,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[52],
- operation_id: "transport.config.update",
- cli_path: "radroots transport config update --kind local-only",
- namespace: "transport",
- mcp_tool: "transport_config_update",
- rust_request: "TransportConfigUpdateRequest",
- rust_result: "TransportConfigUpdateResult",
- json_kind: "transport.config.update",
- description: "Update transport configuration.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportStatusInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[53],
- operation_id: "transport.status.inspect",
- cli_path: "radroots transport status inspect",
- namespace: "transport",
- mcp_tool: "transport_status_inspect",
- rust_request: "TransportStatusInspectRequest",
- rust_result: "TransportStatusInspectResult",
- json_kind: "transport.status.inspect",
- description: "Inspect transport status.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportDeliveryInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[54],
- operation_id: "transport.delivery.inspect",
- cli_path: "radroots transport delivery inspect",
- namespace: "transport",
- mcp_tool: "transport_delivery_inspect",
- rust_request: "TransportDeliveryInspectRequest",
- rust_result: "TransportDeliveryInspectResult",
- json_kind: "transport.delivery.inspect",
- description: "Inspect transport delivery.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::TransportDeliveryRetry,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[55],
- operation_id: "transport.delivery.retry",
- cli_path: "radroots transport delivery retry",
- namespace: "transport",
- mcp_tool: "transport_delivery_retry",
- rust_request: "TransportDeliveryRetryRequest",
- rust_result: "TransportDeliveryRetryResult",
- json_kind: "transport.delivery.retry",
- description: "Retry transport delivery.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: true,
- },
- OperationSpec {
- runtime_operation_id: RuntimeOperationIdV1::DiagnosticsInspect,
- descriptor: radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1[56],
- operation_id: "diagnostics.inspect",
- cli_path: "radroots diagnostics inspect",
- namespace: "diagnostics",
- mcp_tool: "diagnostics_inspect",
- rust_request: "DiagnosticsInspectRequest",
- rust_result: "DiagnosticsInspectResult",
- json_kind: "diagnostics.inspect",
- description: "Inspect runtime diagnostics.",
- role: OperationRole::Any,
- supports_json: true,
- supports_ndjson: false,
- },
-];
diff --git a/src/generated/runtime_contract_target.rs b/src/generated/runtime_contract_target.rs
@@ -1,61 +0,0 @@
-// @generated by radroots_sdk_xtask. Do not edit by hand.
-
-target_operation_contracts! {
- ProfileInspect => (ProfileInspectRequest, ProfileInspectResult, "profile.inspect"),
- ProfileReset => (ProfileResetRequest, ProfileResetResult, "profile.reset"),
- AccountCreate => (AccountCreateRequest, AccountCreateResult, "account.create"),
- AccountImport => (AccountImportRequest, AccountImportResult, "account.import"),
- AccountSelect => (AccountSelectRequest, AccountSelectResult, "account.select"),
- AccountList => (AccountListRequest, AccountListResult, "account.list"),
- AccountRemove => (AccountRemoveRequest, AccountRemoveResult, "account.remove"),
- SignerStatus => (SignerStatusRequest, SignerStatusResult, "signer.status"),
- StoreInspect => (StoreInspectRequest, StoreInspectResult, "store.inspect"),
- StoreBackup => (StoreBackupRequest, StoreBackupResult, "store.backup"),
- StoreRestore => (StoreRestoreRequest, StoreRestoreResult, "store.restore"),
- FarmCreate => (FarmCreateRequest, FarmCreateResult, "farm.create"),
- FarmUpdate => (FarmUpdateRequest, FarmUpdateResult, "farm.update"),
- FarmPublish => (FarmPublishRequest, FarmPublishResult, "farm.publish"),
- FarmGet => (FarmGetRequest, FarmGetResult, "farm.get"),
- FarmList => (FarmListRequest, FarmListResult, "farm.list"),
- ListingCreate => (ListingCreateRequest, ListingCreateResult, "listing.create"),
- ListingUpdate => (ListingUpdateRequest, ListingUpdateResult, "listing.update"),
- ListingPublish => (ListingPublishRequest, ListingPublishResult, "listing.publish"),
- ListingPause => (ListingPauseRequest, ListingPauseResult, "listing.pause"),
- ListingWithdraw => (ListingWithdrawRequest, ListingWithdrawResult, "listing.withdraw"),
- ListingGet => (ListingGetRequest, ListingGetResult, "listing.get"),
- ListingList => (ListingListRequest, ListingListResult, "listing.list"),
- MarketPull => (MarketPullRequest, MarketPullResult, "market.pull"),
- MarketSearch => (MarketSearchRequest, MarketSearchResult, "market.search"),
- MarketGet => (MarketGetRequest, MarketGetResult, "market.get"),
- BasketCreate => (BasketCreateRequest, BasketCreateResult, "basket.create"),
- BasketGet => (BasketGetRequest, BasketGetResult, "basket.get"),
- BasketList => (BasketListRequest, BasketListResult, "basket.list"),
- BasketItemAdd => (BasketItemAddRequest, BasketItemAddResult, "basket.item.add"),
- BasketItemUpdate => (BasketItemUpdateRequest, BasketItemUpdateResult, "basket.item.update"),
- BasketItemRemove => (BasketItemRemoveRequest, BasketItemRemoveResult, "basket.item.remove"),
- BasketQuote => (BasketQuoteRequest, BasketQuoteResult, "basket.quote"),
- TradeProposalSubmit => (TradeProposalSubmitRequest, TradeProposalSubmitResult, "trade.proposal.submit"),
- TradeRevisionPropose => (TradeRevisionProposeRequest, TradeRevisionProposeResult, "trade.revision.propose"),
- TradeCandidateDecide => (TradeCandidateDecideRequest, TradeCandidateDecideResult, "trade.candidate.decide"),
- TradeCancellationSubmit => (TradeCancellationSubmitRequest, TradeCancellationSubmitResult, "trade.cancellation.submit"),
- TradeOperationResume => (TradeOperationResumeRequest, TradeOperationResumeResult, "trade.operation.resume"),
- TradeGet => (TradeGetRequest, TradeGetResult, "trade.get"),
- TradeList => (TradeListRequest, TradeListResult, "trade.list"),
- TradeEvidenceRefresh => (TradeEvidenceRefreshRequest, TradeEvidenceRefreshResult, "trade.evidence.refresh"),
- TradeEvidenceInspect => (TradeEvidenceInspectRequest, TradeEvidenceInspectResult, "trade.evidence.inspect"),
- TradePrivateArtifactSeal => (TradePrivateArtifactSealRequest, TradePrivateArtifactSealResult, "trade.private_artifact.seal"),
- TradePrivateArtifactOpen => (TradePrivateArtifactOpenRequest, TradePrivateArtifactOpenResult, "trade.private_artifact.open"),
- TradePrivateArtifactDelete => (TradePrivateArtifactDeleteRequest, TradePrivateArtifactDeleteResult, "trade.private_artifact.delete"),
- ValidationStatus => (ValidationStatusRequest, ValidationStatusResult, "validation.status"),
- SyncStatus => (SyncStatusRequest, SyncStatusResult, "sync.status"),
- SyncPull => (SyncPullRequest, SyncPullResult, "sync.pull"),
- SyncPush => (SyncPushRequest, SyncPushResult, "sync.push"),
- HealthInspect => (HealthInspectRequest, HealthInspectResult, "health.inspect"),
- TransportCapabilityList => (TransportCapabilityListRequest, TransportCapabilityListResult, "transport.capability.list"),
- TransportConfigInspect => (TransportConfigInspectRequest, TransportConfigInspectResult, "transport.config.inspect"),
- TransportConfigUpdate => (TransportConfigUpdateRequest, TransportConfigUpdateResult, "transport.config.update"),
- TransportStatusInspect => (TransportStatusInspectRequest, TransportStatusInspectResult, "transport.status.inspect"),
- TransportDeliveryInspect => (TransportDeliveryInspectRequest, TransportDeliveryInspectResult, "transport.delivery.inspect"),
- TransportDeliveryRetry => (TransportDeliveryRetryRequest, TransportDeliveryRetryResult, "transport.delivery.retry"),
- DiagnosticsInspect => (DiagnosticsInspectRequest, DiagnosticsInspectResult, "diagnostics.inspect"),
-}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,8 +1,3 @@
#![forbid(unsafe_code)]
pub mod cli;
-pub mod ops;
-pub mod out;
-pub mod registry;
-pub mod runtime;
-pub mod view;
diff --git a/src/main.rs b/src/main.rs
@@ -1,776 +1,117 @@
#![forbid(unsafe_code)]
-use std::io::Write;
use std::process::ExitCode;
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
use clap::Parser;
-use radroots_cli::{ops, registry, runtime};
-use radroots_runtime_contract_v1::{
- ExecutionModeV1, RUNTIME_CONTRACT_NAME_V1, RUNTIME_CONTRACT_VERSION_V1, RuntimeContractErrorV1,
- RuntimeRequestEnvelopeV1,
+use radroots_cli::cli::{
+ HealthCommand, ProfileCommand, TargetCliArgs, TargetCommand, TargetOutputFormat,
};
-use serde_json::json;
+use serde_json::{Value, json};
-use radroots_cli::cli::input::runtime_invocation_args_from_target;
-use radroots_cli::cli::{TargetCliArgs, TargetOutputFormat};
-use radroots_cli::ops::exec::{
- BasketOperationService, CoreOperationService, FarmOperationService, ListingOperationService,
- MarketOperationService, RuntimeOperationService, TradeOperationService,
- ValidationOperationService,
-};
-use radroots_cli::ops::{
- OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat,
- OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService,
- TargetOperationRequest,
-};
-use radroots_cli::out::envelope::{CliExitCode, EnvelopeContext, OutputEnvelope, OutputError};
-use radroots_cli::out::terminal::errors::terminal_error_document;
-use radroots_cli::out::terminal::registry::TerminalRendererRegistryError;
-use radroots_cli::out::terminal::registry::terminal_renderer_registry;
-use radroots_cli::out::terminal::renderer::{
- TerminalRenderContext, TerminalVerbosity, render_terminal_document,
-};
-use radroots_cli::registry::{
- NetworkRequirement, OPERATION_REGISTRY, network_requirement,
- requires_delivery_capable_transport_profile, requires_local_signer_mode,
-};
-use radroots_cli::runtime::config::{
- OutputFormat as RuntimeOutputFormat, RuntimeConfig, SignerBackend, TransportProfileKind,
- Verbosity,
-};
-use radroots_cli::runtime::logging::initialize_logging;
-
-static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
+const OUTPUT_SCHEMA: &str = "radroots.cli.output.v1";
-fn main() -> ExitCode {
- match run() {
- Ok(exit_code) => exit_code,
- Err(error) => {
- let _ = writeln!(std::io::stderr(), "{error}");
- error.exit_code()
- }
- }
-}
-
-fn run() -> Result<ExitCode, runtime::RuntimeError> {
- debug_assert!(registry::registry_linkage_is_valid());
- debug_assert!(ops::adapter_registry_linkage_is_valid());
+#[tokio::main]
+async fn main() -> ExitCode {
let args = TargetCliArgs::parse();
- let mut request =
- TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?;
- let pre_runtime_render_config =
- render_config_from_target_args(&args, args.format.unwrap_or(TargetOutputFormat::Terminal));
- if let Err(error) = validate_pre_runtime_request_contract(&request) {
- let envelope = failure_envelope(&request, error);
- return render_envelope(&envelope, &pre_runtime_render_config);
- }
- let config = match RuntimeConfig::from_system(&runtime_invocation_args_from_target(&args)) {
- Ok(config) => config,
- Err(error) => {
- let envelope = runtime_config_failure_envelope(&request, error);
- return render_envelope(&envelope, &pre_runtime_render_config);
- }
- };
- request.set_output_format(OperationOutputFormat::from(config.output.format));
- let runtime_render_config = render_config_from_runtime(&config);
- let logging = match initialize_logging(&config.logging) {
- Ok(logging) => logging,
- Err(error) => {
- let envelope = runtime_config_failure_envelope(&request, error.into());
- return render_envelope(&envelope, &runtime_render_config);
- }
- };
- let envelope = match validate_request_contract(&request, &config) {
- Ok(()) => execute_request(request, &config, &logging),
- Err(error) => failure_envelope(&request, error),
- };
- render_envelope(&envelope, &runtime_render_config)
-}
-
-fn execute_request(
- request: TargetOperationRequest,
- config: &RuntimeConfig,
- logging: &runtime::logging::LoggingState,
-) -> OutputEnvelope {
- match request {
- TargetOperationRequest::ProfileReset(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::ProfileInspect(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountCreate(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountImport(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountList(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountRemove(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::AccountSelect(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::StoreInspect(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::StoreBackup(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::StoreRestore(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- TargetOperationRequest::SignerStatus(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportCapabilityList(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportConfigInspect(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportConfigUpdate(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportStatusInspect(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportDeliveryInspect(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::TransportDeliveryRetry(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::SyncStatus(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::SyncPull(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::SyncPush(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::DiagnosticsInspect(request) => {
- execute_with(RuntimeOperationService::new(config), request)
- }
- TargetOperationRequest::FarmCreate(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmGet(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmUpdate(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmList(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::FarmPublish(request) => {
- execute_with(FarmOperationService::new(config), request)
- }
- TargetOperationRequest::ListingCreate(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingGet(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingList(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingUpdate(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingPublish(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingPause(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::ListingWithdraw(request) => {
- execute_with(ListingOperationService::new(config), request)
- }
- TargetOperationRequest::MarketPull(request) => {
- execute_with(MarketOperationService::new(config), request)
- }
- TargetOperationRequest::MarketSearch(request) => {
- execute_with(MarketOperationService::new(config), request)
- }
- TargetOperationRequest::MarketGet(request) => {
- execute_with(MarketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketCreate(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketGet(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketList(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketItemAdd(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketItemUpdate(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketItemRemove(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::BasketQuote(request) => {
- execute_with(BasketOperationService::new(config), request)
- }
- TargetOperationRequest::TradeProposalSubmit(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeRevisionPropose(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeCandidateDecide(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeCancellationSubmit(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeOperationResume(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeGet(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeList(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeEvidenceRefresh(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradeEvidenceInspect(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradePrivateArtifactSeal(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradePrivateArtifactOpen(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::TradePrivateArtifactDelete(request) => {
- execute_with(TradeOperationService::new(config), request)
- }
- TargetOperationRequest::ValidationStatus(request) => {
- execute_with(ValidationOperationService::new(config), request)
- }
- TargetOperationRequest::HealthInspect(request) => {
- execute_with(CoreOperationService::new(config, logging), request)
- }
- }
-}
-
-fn execute_with<S, P>(service: S, request: OperationRequest<P>) -> OutputEnvelope
-where
- S: OperationService<P>,
- P: OperationRequestPayload,
- S::Result: OperationResultPayload,
-{
- let operation_id = request.operation_id().to_owned();
- let envelope_context = request
- .context
- .envelope_context(next_request_id(&operation_id));
- match OperationAdapter::new(service)
- .execute(request)
- .and_then(|result| result.to_envelope(envelope_context.clone()))
- {
- Ok(envelope) => envelope,
- Err(error) => {
- OutputEnvelope::failure(operation_id, error.to_output_error(), envelope_context)
- }
- }
-}
-
-fn validate_request_contract(
- request: &TargetOperationRequest,
- config: &RuntimeConfig,
-) -> Result<(), OperationAdapterError> {
- validate_pre_runtime_request_contract(request)?;
- validate_transport_profile_contract(request, config)?;
- validate_signer_mode_contract(request, config)?;
- validate_network_contract(request, config)?;
- Ok(())
-}
-
-fn validate_pre_runtime_request_contract(
- request: &TargetOperationRequest,
-) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- if matches!(
- request.context().output_format,
- OperationOutputFormat::Ndjson
- ) && !spec.supports_ndjson
- {
- return Err(OperationAdapterError::InvalidInput {
- operation_id: spec.operation_id.to_owned(),
- message: format!("`{}` does not support --format ndjson", spec.cli_path),
- });
- }
- if request.context().dry_run && !spec.supports_dry_run() {
- return Err(OperationAdapterError::InvalidInput {
- operation_id: spec.operation_id.to_owned(),
- message: format!("`{}` does not support --dry-run", spec.cli_path),
- });
- }
- validate_idempotency_contract(request)?;
- if !request.context().dry_run
- && spec.requires_approval()
- && !request.context().has_operator_approval()
- {
- return Err(OperationAdapterError::approval_required(spec.operation_id));
- }
- Ok(())
-}
-
-fn validate_idempotency_contract(
- request: &TargetOperationRequest,
-) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- RuntimeRequestEnvelopeV1 {
- contract: RUNTIME_CONTRACT_NAME_V1.to_owned(),
- contract_version: RUNTIME_CONTRACT_VERSION_V1,
- operation_id: spec.runtime_operation_id,
- operation_schema_version: spec.descriptor.schema_version,
- request_id: "cli-pre-runtime-contract-validation".to_owned(),
- actor_pubkey: None,
- idempotency_key: request.context().idempotency_key.clone(),
- approval: request.context().approval_proof.clone(),
- execution_mode: ExecutionModeV1::Embedded,
- request_json: "{}".to_owned(),
- }
- .validate()
- .map_err(|error| idempotency_contract_error(spec.operation_id, error))
-}
-
-fn idempotency_contract_error(
- operation_id: &str,
- error: RuntimeContractErrorV1,
-) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message: error.to_string(),
- }
-}
-
-fn validate_signer_mode_contract(
- request: &TargetOperationRequest,
- config: &RuntimeConfig,
-) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- if matches!(config.signer.backend, SignerBackend::Myc)
- && requires_local_signer_mode_for_transport_profile(spec.operation_id, config)
- {
- return Err(OperationAdapterError::SignerModeDeferred {
- operation_id: spec.operation_id.to_owned(),
- message: format!(
- "`{}` cannot run with signer mode `myc`; use signer mode `local`",
- spec.cli_path
- ),
- });
- }
- Ok(())
-}
-
-fn validate_network_contract(
- request: &TargetOperationRequest,
- config: &RuntimeConfig,
-) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- let requirement = network_requirement(spec.operation_id);
- match request.context().network_mode {
- OperationNetworkMode::Default => Ok(()),
- OperationNetworkMode::Offline => {
- if allows_offline_local_mutation(spec.operation_id) {
- return Ok(());
- }
- if let NetworkRequirement::External {
- dry_run_requires_network,
- } = requirement
- && (!request.context().dry_run || dry_run_requires_network)
- {
- return Err(OperationAdapterError::OfflineForbidden {
- operation_id: spec.operation_id.to_owned(),
- message: format!(
- "`{}` requires transport, provider, or workflow network access",
- spec.cli_path
- ),
- });
- }
- Ok(())
- }
- OperationNetworkMode::Online => {
- if let NetworkRequirement::External {
- dry_run_requires_network,
- } = requirement
- && (!request.context().dry_run || dry_run_requires_network)
- && requires_pre_runtime_transport_target(spec.operation_id)
- && let Some(reason) = transport_profile_delivery_unavailable_reason(config)
- {
- return Err(OperationAdapterError::NetworkUnavailable {
- operation_id: spec.operation_id.to_owned(),
- message: format!(
- "`{}` requires a delivery-capable transport profile for online execution: {reason}",
- spec.cli_path,
- ),
- });
- }
- Ok(())
- }
- }
-}
-
-fn requires_local_signer_mode_for_transport_profile(
- operation_id: &str,
- config: &RuntimeConfig,
-) -> bool {
- let _ = config;
- requires_local_signer_mode(operation_id)
-}
-
-fn requires_pre_runtime_transport_target(operation_id: &str) -> bool {
- !requires_delivery_capable_transport_profile(operation_id)
-}
-
-fn allows_offline_local_mutation(operation_id: &str) -> bool {
- matches!(operation_id, "listing.publish")
-}
-
-fn validate_transport_profile_contract(
- request: &TargetOperationRequest,
- config: &RuntimeConfig,
-) -> Result<(), OperationAdapterError> {
- let spec = request.spec();
- if !requires_delivery_capable_transport_profile(spec.operation_id) {
- return Ok(());
- }
- if request.context().dry_run
- || matches!(
- request.context().network_mode,
- OperationNetworkMode::Offline
- )
- {
- return Ok(());
- }
- if matches!(
- config.transport.profile,
- TransportProfileKind::LocalOnly | TransportProfileKind::Reticulum
- ) {
- return Err(OperationAdapterError::NetworkUnavailable {
- operation_id: spec.operation_id.to_owned(),
- message: format!(
- "`{}` requires a delivery-capable transport profile; active profile `{}` cannot deliver",
- spec.cli_path,
- config.transport.profile.as_str()
- ),
- });
- }
- if matches!(
- config.transport.profile,
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
- ) && config.transport.nostr_relay_urls.is_empty()
- {
- return Err(OperationAdapterError::NetworkUnavailable {
- operation_id: spec.operation_id.to_owned(),
- message: format!(
- "`{}` requires at least one configured Nostr relay in the active transport profile",
- spec.cli_path
- ),
- });
- }
- Ok(())
-}
-
-fn transport_profile_delivery_unavailable_reason(config: &RuntimeConfig) -> Option<String> {
- match config.transport.profile {
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => {
- config.transport.nostr_relay_urls.is_empty().then(|| {
- format!(
- "active {} transport profile has no configured Nostr relay targets",
- config.transport.profile.as_str()
- )
- })
- }
- TransportProfileKind::LocalOnly => {
- Some("active local_only transport profile cannot deliver".to_owned())
- }
- TransportProfileKind::Reticulum => {
- Some("active reticulum transport profile cannot deliver".to_owned())
- }
- }
-}
-
-fn failure_envelope(
- request: &TargetOperationRequest,
- error: OperationAdapterError,
-) -> OutputEnvelope {
- OutputEnvelope::failure(
- request.operation_id(),
- error.to_output_error(),
- request
- .context()
- .envelope_context(next_request_id(request.operation_id())),
- )
-}
-
-fn runtime_config_failure_envelope(
- request: &TargetOperationRequest,
- error: runtime::RuntimeError,
-) -> OutputEnvelope {
- OutputEnvelope::failure(
- request.operation_id(),
- OutputError::new(
- "invalid_input",
- error.to_string(),
- CliExitCode::InvalidInput,
+ let operation_id = args.command.operation_id();
+ let format = args.format.unwrap_or(TargetOutputFormat::Terminal);
+
+ let result = execute(&args.command).await;
+ let (status, data, error, exit_code) = match result {
+ Ok(data) => ("ok", data, Value::Null, ExitCode::SUCCESS),
+ Err(message) => (
+ "error",
+ Value::Null,
+ json!({
+ "code": "unsupported_operation",
+ "message": message,
+ "recovery": "use `radroots health inspect` to inspect the release-v1 host surface"
+ }),
+ ExitCode::from(2),
),
- request
- .context()
- .envelope_context(next_request_id(request.operation_id())),
- )
-}
-
-fn next_request_id(operation_id: &str) -> String {
- let sequence = REQUEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_nanos())
- .unwrap_or_default();
- format!(
- "req_{}_{}_{}_{}",
- operation_id.replace('.', "_"),
- std::process::id(),
- timestamp,
- sequence
- )
-}
-
-#[derive(Debug, Clone)]
-struct EnvelopeRenderConfig {
- format: RenderOutputFormat,
- terminal: TerminalRenderContext,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum RenderOutputFormat {
- Terminal,
- Json,
- Ndjson,
-}
-
-fn render_config_from_target_args(
- args: &TargetCliArgs,
- format: TargetOutputFormat,
-) -> EnvelopeRenderConfig {
- EnvelopeRenderConfig {
- format: match format {
- TargetOutputFormat::Terminal => RenderOutputFormat::Terminal,
- TargetOutputFormat::Json => RenderOutputFormat::Json,
- TargetOutputFormat::Ndjson => RenderOutputFormat::Ndjson,
- },
- terminal: TerminalRenderContext {
- verbosity: terminal_verbosity_from_flags(args.quiet, args.verbose, args.trace),
- width: 80,
- dry_run: args.dry_run,
- },
- }
-}
-
-fn render_config_from_runtime(config: &RuntimeConfig) -> EnvelopeRenderConfig {
- EnvelopeRenderConfig {
- format: match config.output.format {
- RuntimeOutputFormat::Terminal => RenderOutputFormat::Terminal,
- RuntimeOutputFormat::Json => RenderOutputFormat::Json,
- RuntimeOutputFormat::Ndjson => RenderOutputFormat::Ndjson,
- },
- terminal: TerminalRenderContext {
- verbosity: terminal_verbosity_from_runtime(config.output.verbosity),
- width: 80,
- dry_run: config.output.dry_run,
- },
- }
-}
-
-fn terminal_verbosity_from_flags(quiet: bool, verbose: bool, trace: bool) -> TerminalVerbosity {
- if trace {
- TerminalVerbosity::Trace
- } else if verbose {
- TerminalVerbosity::Verbose
- } else if quiet {
- TerminalVerbosity::Quiet
- } else {
- TerminalVerbosity::Normal
- }
-}
-
-fn terminal_verbosity_from_runtime(verbosity: Verbosity) -> TerminalVerbosity {
- match verbosity {
- Verbosity::Quiet => TerminalVerbosity::Quiet,
- Verbosity::Normal => TerminalVerbosity::Normal,
- Verbosity::Verbose => TerminalVerbosity::Verbose,
- Verbosity::Trace => TerminalVerbosity::Trace,
- }
-}
+ };
-fn render_envelope(
- envelope: &OutputEnvelope,
- config: &EnvelopeRenderConfig,
-) -> Result<ExitCode, runtime::RuntimeError> {
- match config.format {
- RenderOutputFormat::Terminal => render_terminal_envelope(envelope, &config.terminal),
- RenderOutputFormat::Json => {
- let stdout = std::io::stdout();
- let mut handle = stdout.lock();
- serde_json::to_writer_pretty(&mut handle, envelope)?;
- writeln!(handle)?;
- Ok(envelope_exit_code(envelope))
- }
- RenderOutputFormat::Ndjson => {
- let stdout = std::io::stdout();
- let mut handle = stdout.lock();
- for frame in envelope.to_ndjson_frames() {
- serde_json::to_writer(&mut handle, &frame)?;
- writeln!(handle)?;
+ let envelope = json!({
+ "schema": OUTPUT_SCHEMA,
+ "operation_id": operation_id,
+ "status": status,
+ "data": data,
+ "error": error,
+ });
+ render(format, operation_id, status, &envelope);
+ exit_code
+}
+
+async fn execute(command: &TargetCommand) -> Result<Value, String> {
+ match command {
+ TargetCommand::Profile(args) if matches!(args.command, ProfileCommand::Inspect) => {
+ inspect_local_profile().await
+ }
+ TargetCommand::Health(args) if matches!(args.command, HealthCommand::Inspect) => {
+ inspect_health().await
+ }
+ _ => Err(format!(
+ "`{}` is not available in the crates-release-v1 CLI host",
+ command.operation_id()
+ )),
+ }
+}
+
+async fn inspect_local_profile() -> Result<Value, String> {
+ let client = radroots::client::memory()
+ .build()
+ .map_err(|error| format!("failed to construct the local SDK client: {error}"))?;
+ let profile = radroots::client::local_only();
+ client
+ .close()
+ .await
+ .map_err(|error| format!("failed to close the local SDK client: {error}"))?;
+
+ Ok(json!({
+ "profile": "local_only",
+ "network_enabled": !profile.is_local_only(),
+ "storage": "memory",
+ "artifact_graph": "registry"
+ }))
+}
+
+async fn inspect_health() -> Result<Value, String> {
+ let profile = inspect_local_profile().await?;
+ Ok(json!({
+ "ready": true,
+ "release": "crates-v1",
+ "profile": profile
+ }))
+}
+
+fn render(format: TargetOutputFormat, operation_id: &str, status: &str, envelope: &Value) {
+ match format {
+ TargetOutputFormat::Json | TargetOutputFormat::Ndjson => println!("{envelope}"),
+ TargetOutputFormat::Terminal => {
+ if status == "ok" {
+ println!("{operation_id}: ready");
+ } else {
+ let message = envelope["error"]["message"]
+ .as_str()
+ .unwrap_or("operation failed");
+ eprintln!("{operation_id}: {message}");
}
- Ok(envelope_exit_code(envelope))
}
}
}
-fn render_terminal_envelope(
- envelope: &OutputEnvelope,
- cx: &TerminalRenderContext,
-) -> Result<ExitCode, runtime::RuntimeError> {
- let registry = terminal_renderer_registry();
- if let Err(error) = registry.validate_against_operations(OPERATION_REGISTRY) {
- let failure = terminal_registry_failure_envelope(envelope, error);
- render_terminal_failure_document(&failure, cx)?;
- return Ok(envelope_exit_code(&failure));
- }
- let renderer = match registry.renderer_for(envelope.operation_id.as_str()) {
- Ok(renderer) => renderer,
- Err(error) => {
- let failure = terminal_registry_failure_envelope(envelope, error);
- render_terminal_failure_document(&failure, cx)?;
- return Ok(envelope_exit_code(&failure));
- }
- };
- let document = renderer.render(envelope, cx);
- let rendered = render_terminal_document(&document, cx);
- if envelope.errors.is_empty() {
- let stdout = std::io::stdout();
- let mut handle = stdout.lock();
- writeln!(handle, "{rendered}")?;
- } else {
- let stderr = std::io::stderr();
- let mut handle = stderr.lock();
- writeln!(handle, "{rendered}")?;
- }
- Ok(envelope_exit_code(envelope))
-}
-
-fn render_terminal_failure_document(
- envelope: &OutputEnvelope,
- cx: &TerminalRenderContext,
-) -> Result<(), runtime::RuntimeError> {
- let document = terminal_error_document(envelope);
- let rendered = render_terminal_document(&document, cx);
- let stderr = std::io::stderr();
- let mut handle = stderr.lock();
- writeln!(handle, "{rendered}")?;
- Ok(())
-}
-
-fn terminal_registry_failure_envelope(
- envelope: &OutputEnvelope,
- error: TerminalRendererRegistryError,
-) -> OutputEnvelope {
- let mut output_error = OutputError::new(
- "internal_error",
- format!(
- "terminal renderer registry invariant failed for {}",
- envelope.operation_id
- ),
- CliExitCode::InternalError,
- );
- output_error.detail = Some(json!({
- "class": "internal",
- "operation_id": envelope.operation_id,
- "violations": error.violations().iter().map(|violation| {
- json!({
- "kind": violation.kind(),
- "operation_id": violation.operation_id(),
- "count": violation.count(),
- })
- }).collect::<Vec<_>>(),
- }));
- let mut context = EnvelopeContext::new(envelope.request_id.clone(), envelope.dry_run);
- context.output_format = envelope.output_format;
- context.correlation_id = envelope.correlation_id.clone();
- context.idempotency_key = envelope.idempotency_key.clone();
- context.actor = envelope.actor.clone();
- OutputEnvelope::failure(envelope.operation_id.clone(), output_error, context)
-}
-
-fn envelope_exit_code(envelope: &OutputEnvelope) -> ExitCode {
- envelope
- .errors
- .first()
- .map(|error| ExitCode::from(error.exit_code))
- .unwrap_or_else(|| ExitCode::from(0))
-}
-
-fn operation_config_error(error: OperationAdapterError) -> runtime::RuntimeError {
- runtime::RuntimeError::Config(error.to_string())
-}
-
#[cfg(test)]
mod tests {
- use serde_json::Value;
-
- use radroots_cli::out::envelope::{EnvelopeContext, OutputEnvelope, OutputStatus};
- use radroots_cli::out::terminal::errors::terminal_error_document;
- use radroots_cli::out::terminal::registry::TerminalRendererRegistry;
- use radroots_cli::out::terminal::renderer::{TerminalRenderContext, render_terminal_document};
-
use super::*;
#[test]
- fn terminal_registry_failure_envelope_is_structured_internal_error() {
- let original = OutputEnvelope::success(
- "profile.inspect",
- Value::Null,
- EnvelopeContext::new("req_test", false),
- );
- let error = TerminalRendererRegistry::new()
- .validate_operation_ids(["profile.inspect"])
- .expect_err("missing renderer should fail");
- let failure = terminal_registry_failure_envelope(&original, error);
-
- assert_eq!(failure.status, OutputStatus::Error);
- assert_eq!(failure.reason_code.as_deref(), Some("internal_error"));
- assert_eq!(failure.errors[0].code, "internal_error");
- let detail = failure.errors[0].detail.as_ref().expect("error detail");
- assert_eq!(detail["violations"][0]["kind"], "missing");
-
- let rendered = render_terminal_document(
- &terminal_error_document(&failure),
- &TerminalRenderContext::default(),
- );
- assert!(rendered.starts_with("✕ Command failed\n"));
- assert!(
- rendered.contains(
- "Reason terminal renderer registry invariant failed for profile.inspect"
- )
- );
- assert!(!rendered.contains("RuntimeError::Config"));
+ fn unsupported_operations_fail_closed() {
+ let args = TargetCliArgs::try_parse_from(["radroots", "account", "list"])
+ .expect("valid resource command");
+ let runtime = tokio::runtime::Runtime::new().expect("runtime");
+ let error = runtime
+ .block_on(execute(&args.command))
+ .expect_err("unsupported operation");
+ assert!(error.contains("account.list"));
}
}
diff --git a/src/ops/adapter.rs b/src/ops/adapter.rs
@@ -1,34 +0,0 @@
-use super::error::OperationAdapterError;
-use super::request::{OperationRequest, OperationRequestPayload};
-use super::result::{OperationResult, OperationResultPayload};
-
-pub trait OperationService<P: OperationRequestPayload> {
- type Result: OperationResultPayload;
-
- fn execute(
- &self,
- request: OperationRequest<P>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError>;
-}
-
-#[derive(Debug, Clone)]
-pub struct OperationAdapter<S> {
- service: S,
-}
-
-impl<S> OperationAdapter<S> {
- pub fn new(service: S) -> Self {
- Self { service }
- }
-
- pub fn execute<P>(
- &self,
- request: OperationRequest<P>,
- ) -> Result<OperationResult<<S as OperationService<P>>::Result>, OperationAdapterError>
- where
- P: OperationRequestPayload,
- S: OperationService<P>,
- {
- self.service.execute(request)
- }
-}
diff --git a/src/ops/context.rs b/src/ops/context.rs
@@ -1,209 +0,0 @@
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_runtime_contract_v1::{ApprovalProofV1, RuntimeContractErrorV1};
-
-use crate::cli::{TargetCliArgs, TargetOutputFormat};
-use crate::ops::OperationAdapterError;
-use crate::out::envelope::{EnvelopeActor, EnvelopeContext, OutputFormat as EnvelopeOutputFormat};
-use crate::runtime::config::OutputFormat as RuntimeOutputFormat;
-
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum OperationOutputFormat {
- #[default]
- Terminal,
- Json,
- Ndjson,
-}
-
-impl From<TargetOutputFormat> for OperationOutputFormat {
- fn from(format: TargetOutputFormat) -> Self {
- match format {
- TargetOutputFormat::Terminal => Self::Terminal,
- TargetOutputFormat::Json => Self::Json,
- TargetOutputFormat::Ndjson => Self::Ndjson,
- }
- }
-}
-
-impl From<RuntimeOutputFormat> for OperationOutputFormat {
- fn from(format: RuntimeOutputFormat) -> Self {
- match format {
- RuntimeOutputFormat::Terminal => Self::Terminal,
- RuntimeOutputFormat::Json => Self::Json,
- RuntimeOutputFormat::Ndjson => Self::Ndjson,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum OperationNetworkMode {
- #[default]
- Default,
- Offline,
- Online,
-}
-
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
-pub enum OperationInputMode {
- #[default]
- PromptingAllowed,
- NoInput,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct OperationContext {
- pub output_format: OperationOutputFormat,
- pub account_id: Option<String>,
- pub network_mode: OperationNetworkMode,
- pub dry_run: bool,
- pub idempotency_key: Option<String>,
- pub correlation_id: Option<String>,
- pub approval_proof: Option<ApprovalProofV1>,
- pub yes: bool,
- pub input_mode: OperationInputMode,
- pub quiet: bool,
- pub verbose: bool,
- pub trace: bool,
-}
-
-impl OperationContext {
- pub fn from_target_args(
- args: &TargetCliArgs,
- operation_id: &'static str,
- ) -> Result<Self, OperationAdapterError> {
- Ok(Self {
- output_format: args
- .format
- .map(OperationOutputFormat::from)
- .unwrap_or_default(),
- account_id: args.account_id.clone(),
- network_mode: if args.offline {
- OperationNetworkMode::Offline
- } else if args.online {
- OperationNetworkMode::Online
- } else {
- OperationNetworkMode::Default
- },
- dry_run: args.dry_run,
- idempotency_key: args.idempotency_key.clone(),
- correlation_id: args.correlation_id.clone(),
- approval_proof: parse_approval_proof(args.approval_proof.as_deref(), operation_id)?,
- yes: args.yes,
- input_mode: if args.no_input {
- OperationInputMode::NoInput
- } else {
- OperationInputMode::PromptingAllowed
- },
- quiet: args.quiet,
- verbose: args.verbose,
- trace: args.trace,
- })
- }
-
- pub fn envelope_context(&self, request_id: impl Into<String>) -> EnvelopeContext {
- let mut context = EnvelopeContext::new(request_id, self.dry_run);
- context.output_format = match self.output_format {
- OperationOutputFormat::Terminal => EnvelopeOutputFormat::Terminal,
- OperationOutputFormat::Json => EnvelopeOutputFormat::Json,
- OperationOutputFormat::Ndjson => EnvelopeOutputFormat::Ndjson,
- };
- context.correlation_id = self.correlation_id.clone();
- context.idempotency_key = self.idempotency_key.clone();
- context.actor = self.account_id.as_ref().map(|account_id| EnvelopeActor {
- account_id: account_id.clone(),
- role: "account".to_owned(),
- });
- context
- }
-
- pub fn has_operator_approval(&self) -> bool {
- self.yes || self.approval_proof.is_some()
- }
-}
-
-fn parse_approval_proof(
- value: Option<&str>,
- operation_id: &'static str,
-) -> Result<Option<ApprovalProofV1>, OperationAdapterError> {
- let Some(value) = value.map(str::trim) else {
- return Ok(None);
- };
- if value.is_empty() {
- return Err(invalid_approval(
- operation_id,
- "approval proof JSON must not be empty",
- ));
- }
- let proof = serde_json::from_str::<ApprovalProofV1>(value).map_err(|error| {
- invalid_approval(
- operation_id,
- format!("approval proof must be valid ApprovalProofV1 JSON: {error}"),
- )
- })?;
- validate_approval_proof(&proof, operation_id)?;
- Ok(Some(proof))
-}
-
-fn validate_approval_proof(
- proof: &ApprovalProofV1,
- operation_id: &'static str,
-) -> Result<(), OperationAdapterError> {
- if proof.operation_id.as_str() != operation_id {
- return Err(invalid_approval(
- operation_id,
- format!(
- "approval proof operation `{}` does not match request operation `{operation_id}`",
- proof.operation_id.as_str()
- ),
- ));
- }
- proof
- .request_digest
- .validate()
- .map_err(|error| invalid_approval(operation_id, runtime_contract_error(error)))?;
- for (field, value) in [
- ("proof_id", proof.proof_id.as_str()),
- ("signer_pubkey", proof.signer_pubkey.as_str()),
- ("signature", proof.signature.as_str()),
- ] {
- if value.trim().is_empty() {
- return Err(invalid_approval(
- operation_id,
- format!("approval proof `{field}` must not be empty"),
- ));
- }
- }
- if proof.signed_at_unix_ms == 0 {
- return Err(invalid_approval(
- operation_id,
- "approval proof `signed_at_unix_ms` must be nonzero",
- ));
- }
- if let Some(expires_at_unix_ms) = proof.expires_at_unix_ms {
- let now_unix_ms = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_millis() as u64)
- .unwrap_or_default();
- if expires_at_unix_ms <= now_unix_ms {
- return Err(OperationAdapterError::ApprovalRequired {
- operation_id: operation_id.to_owned(),
- message: "approval proof has expired".to_owned(),
- });
- }
- }
- Ok(())
-}
-
-fn invalid_approval(
- operation_id: &'static str,
- message: impl Into<String>,
-) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message: message.into(),
- }
-}
-
-fn runtime_contract_error(error: RuntimeContractErrorV1) -> String {
- error.to_string()
-}
diff --git a/src/ops/error.rs b/src/ops/error.rs
@@ -1,978 +0,0 @@
-use std::io::ErrorKind;
-
-use radroots_protocol::error::v1::{Class as ErrorClass, RecoveryAction};
-use radroots_sdk::Error as SdkError;
-use serde_json::{Map, Value, json};
-
-use crate::out::envelope::{CliExitCode, OutputError};
-use crate::runtime::RuntimeError;
-use crate::runtime::account::AccountRuntimeFailure;
-use crate::runtime::sdk::CliSdkAdapterError;
-use crate::view::runtime::CommandDisposition;
-
-#[derive(Debug, thiserror::Error, PartialEq, Eq)]
-pub enum OperationAdapterError {
- #[error("unknown operation `{0}`")]
- UnknownOperation(String),
- #[error(
- "operation `{operation_id}` registry request `{registry_request}` does not match adapter request `{adapter_request}`"
- )]
- RequestTypeMismatch {
- operation_id: String,
- registry_request: String,
- adapter_request: String,
- },
- #[error(
- "operation `{operation_id}` registry result `{registry_result}` does not match adapter result `{adapter_result}`"
- )]
- ResultTypeMismatch {
- operation_id: String,
- registry_result: String,
- adapter_result: String,
- },
- #[error("failed to serialize operation result: {0}")]
- Serialization(String),
- #[error("invalid operation input for `{operation_id}`: {message}")]
- InvalidInput {
- operation_id: String,
- message: String,
- },
- #[error("resource not found for `{operation_id}`: {message}")]
- NotFound {
- operation_id: String,
- message: String,
- },
- #[error("validation failed for `{operation_id}`: {message}")]
- ValidationFailed {
- operation_id: String,
- message: String,
- },
- #[error("approval required for `{operation_id}`: {message}")]
- ApprovalRequired {
- operation_id: String,
- message: String,
- },
- #[error("operation `{operation_id}` is forbidden while offline: {message}")]
- OfflineForbidden {
- operation_id: String,
- message: String,
- },
- #[error("operation `{operation_id}` cannot run online: {message}")]
- NetworkUnavailable {
- operation_id: String,
- message: String,
- },
- #[error("account unresolved for `{operation_id}`: {message}")]
- AccountUnresolved {
- operation_id: String,
- message: String,
- },
- #[error("account is watch-only for `{operation_id}`: {message}")]
- AccountWatchOnly {
- operation_id: String,
- message: String,
- },
- #[error("account mismatch for `{operation_id}`: {message}")]
- AccountMismatch {
- operation_id: String,
- message: String,
- },
- #[error("signer unconfigured for `{operation_id}`: {message}")]
- SignerUnconfigured {
- operation_id: String,
- message: String,
- },
- #[error("signer unavailable for `{operation_id}`: {message}")]
- SignerUnavailable {
- operation_id: String,
- message: String,
- },
- #[error("signer mode deferred for `{operation_id}`: {message}")]
- SignerModeDeferred {
- operation_id: String,
- message: String,
- },
- #[error("provider unconfigured for `{operation_id}`: {message}")]
- ProviderUnconfigured {
- operation_id: String,
- message: String,
- },
- #[error("provider unavailable for `{operation_id}`: {message}")]
- ProviderUnavailable {
- operation_id: String,
- message: String,
- },
- #[error("operation `{operation_id}` is unavailable: {message}")]
- OperationUnavailable {
- operation_id: String,
- message: String,
- },
- #[error("operation `{operation_id}` is not implemented: {message}")]
- NotImplemented {
- operation_id: String,
- message: String,
- },
- #[error("operation `{}` failed: {}", .0.operation_id, .0.message)]
- DetailedFailure(Box<OperationDetailedFailure>),
- #[error("operation runtime error: {0}")]
- Runtime(String),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct OperationDetailedFailure {
- pub operation_id: String,
- pub code: String,
- pub class: String,
- pub message: String,
- pub exit_code: CliExitCode,
- pub detail_json: String,
-}
-
-impl OperationAdapterError {
- fn detailed_failure(
- operation_id: String,
- code: String,
- class: String,
- message: String,
- exit_code: CliExitCode,
- detail_json: String,
- ) -> Self {
- Self::DetailedFailure(Box::new(OperationDetailedFailure {
- operation_id,
- code,
- class,
- message,
- exit_code,
- detail_json,
- }))
- }
-
- pub fn approval_required(operation_id: &str) -> Self {
- Self::ApprovalRequired {
- operation_id: operation_id.to_owned(),
- message: "missing required V1 approval proof or --yes confirmation".to_owned(),
- }
- }
-
- pub fn from_command_disposition(
- operation_id: &str,
- disposition: CommandDisposition,
- message: String,
- ) -> Self {
- match disposition {
- CommandDisposition::Success => Self::Runtime(message),
- CommandDisposition::NotFound => Self::NotFound {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::ValidationFailed => Self::ValidationFailed {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::Unconfigured => Self::unconfigured(operation_id, message),
- CommandDisposition::ExternalUnavailable => Self::unavailable(operation_id, message),
- CommandDisposition::Unsupported => Self::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::InternalError => Self::Runtime(message),
- }
- }
-
- pub fn unconfigured(operation_id: &str, message: String) -> Self {
- classify_runtime_failure(
- operation_id,
- message,
- RuntimeFailureAvailability::Unconfigured,
- )
- }
-
- pub fn operation_unavailable_with_detail(
- operation_id: &str,
- message: String,
- detail: Value,
- ) -> Self {
- Self::detailed_failure(
- operation_id.to_owned(),
- "operation_unavailable".to_owned(),
- "operation".to_owned(),
- message,
- CliExitCode::RuntimeUnavailable,
- detail.to_string(),
- )
- }
-
- pub fn not_found_with_detail(operation_id: &str, message: String, detail: Value) -> Self {
- Self::detailed_failure(
- operation_id.to_owned(),
- "not_found".to_owned(),
- "resource".to_owned(),
- message,
- CliExitCode::NotFound,
- detail.to_string(),
- )
- }
-
- pub fn not_implemented(operation_id: &str, message: String) -> Self {
- Self::NotImplemented {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
-
- pub fn not_implemented_with_detail(operation_id: &str, message: String, detail: Value) -> Self {
- Self::detailed_failure(
- operation_id.to_owned(),
- "not_implemented".to_owned(),
- "operation".to_owned(),
- message,
- CliExitCode::RuntimeUnavailable,
- detail.to_string(),
- )
- }
-
- pub fn network_unavailable_with_detail(
- operation_id: &str,
- message: String,
- detail: Value,
- ) -> Self {
- Self::detailed_failure(
- operation_id.to_owned(),
- "network_unavailable".to_owned(),
- "network".to_owned(),
- message,
- CliExitCode::SyncOrNetworkFailure,
- detail.to_string(),
- )
- }
-
- pub fn validation_failed_with_detail(
- operation_id: &str,
- message: String,
- detail: Value,
- ) -> Self {
- Self::detailed_failure(
- operation_id.to_owned(),
- "validation_failed".to_owned(),
- "validation".to_owned(),
- message,
- CliExitCode::ValidationFailed,
- detail.to_string(),
- )
- }
-
- pub fn unavailable(operation_id: &str, message: String) -> Self {
- classify_runtime_failure(
- operation_id,
- message,
- RuntimeFailureAvailability::Unavailable,
- )
- }
-
- pub fn runtime_failure(operation_id: &str, error: RuntimeError) -> Self {
- let message = error.to_string();
- let lowered = message.to_ascii_lowercase();
- match &error {
- RuntimeError::Io(io_error) if io_error.kind() == ErrorKind::NotFound => {
- Self::NotFound {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Config(_) if looks_like_not_found(&lowered) => Self::NotFound {
- operation_id: operation_id.to_owned(),
- message,
- },
- RuntimeError::Account(failure) => account_runtime_failure(operation_id, failure),
- RuntimeError::Config(_)
- if contains_any(
- &lowered,
- &[
- "no local account",
- "account selector",
- "account selection",
- "account mismatch",
- "did not match any local account",
- "unresolved account",
- ],
- ) =>
- {
- classify_runtime_failure(
- operation_id,
- message,
- RuntimeFailureAvailability::Unconfigured,
- )
- }
- RuntimeError::Config(_) if looks_like_signer_failure(&lowered) => {
- Self::SignerUnconfigured {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Config(_) if looks_like_validation_failure(&lowered) => {
- Self::ValidationFailed {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Network(_) if looks_like_auth_failure(&lowered) => {
- auth_runtime_failure(operation_id, message, &lowered)
- }
- RuntimeError::Network(_) if looks_like_signer_failure(&lowered) => {
- Self::SignerUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Network(_) if looks_like_provider_failure(&lowered) => {
- Self::ProviderUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Network(_) if looks_like_operation_failure(&lowered) => {
- Self::OperationUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeError::Network(_) => Self::NetworkUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- },
- RuntimeError::Accounts(_) => classify_runtime_failure(
- operation_id,
- message,
- RuntimeFailureAvailability::Unavailable,
- ),
- _ => Self::Runtime(message),
- }
- }
-
- pub fn sdk_adapter_failure(operation_id: &str, error: CliSdkAdapterError) -> Self {
- match error {
- CliSdkAdapterError::Runtime(error) => Self::runtime_failure(operation_id, error),
- CliSdkAdapterError::Sdk(error) => Self::sdk_failure(operation_id, error),
- CliSdkAdapterError::Sync(error) => Self::Runtime(error.to_string()),
- CliSdkAdapterError::Storage(error) => Self::Runtime(error.to_string()),
- CliSdkAdapterError::Transport(error) => Self::NetworkUnavailable {
- operation_id: operation_id.to_owned(),
- message: error.to_string(),
- },
- CliSdkAdapterError::Io(error) => Self::Runtime(error.to_string()),
- }
- }
-
- pub fn sdk_failure(operation_id: &str, error: SdkError) -> Self {
- let report = error.to_report();
- let code = report.code().as_str().to_owned();
- let class = sdk_error_class_name(report.class()).to_owned();
- let message = report.message().as_str().to_owned();
- let exit_code = sdk_error_exit_code(report.class());
- let mut detail = serde_json::to_value(&report).unwrap_or_else(|_| json!({}));
- let actions = sdk_recovery_next_actions(operation_id, report.recovery_actions());
- if !actions.is_empty()
- && let Some(detail) = detail.as_object_mut()
- {
- detail.insert(
- "actions".to_owned(),
- Value::Array(actions.into_iter().map(Value::String).collect()),
- );
- }
- Self::detailed_failure(
- operation_id.to_owned(),
- code,
- class,
- message,
- exit_code,
- detail.to_string(),
- )
- }
-
- pub fn to_output_error(&self) -> OutputError {
- match self {
- Self::ApprovalRequired { message, .. } => OutputError::new(
- "approval_required",
- message.clone(),
- CliExitCode::ApprovalRequiredOrDenied,
- ),
- Self::InvalidInput { message, .. } => {
- OutputError::new("invalid_input", message.clone(), CliExitCode::InvalidInput)
- }
- Self::NotFound {
- operation_id,
- message,
- } => runtime_output_error(
- "not_found",
- operation_id,
- "resource",
- message,
- CliExitCode::NotFound,
- ),
- Self::ValidationFailed {
- operation_id,
- message,
- } => runtime_output_error(
- "validation_failed",
- operation_id,
- "validation",
- message,
- CliExitCode::ValidationFailed,
- ),
- Self::OfflineForbidden {
- operation_id,
- message,
- } => runtime_output_error(
- "offline_forbidden",
- operation_id,
- "network",
- message,
- CliExitCode::SyncOrNetworkFailure,
- ),
- Self::NetworkUnavailable {
- operation_id,
- message,
- } => runtime_output_error(
- "network_unavailable",
- operation_id,
- "network",
- message,
- CliExitCode::SyncOrNetworkFailure,
- ),
- Self::AccountUnresolved {
- operation_id,
- message,
- } => runtime_output_error(
- "account_unresolved",
- operation_id,
- "account",
- message,
- CliExitCode::AuthorizationFailed,
- ),
- Self::AccountWatchOnly {
- operation_id,
- message,
- } => runtime_output_error(
- "account_watch_only",
- operation_id,
- "account",
- message,
- CliExitCode::SignerUnavailable,
- ),
- Self::AccountMismatch {
- operation_id,
- message,
- } => runtime_output_error(
- "account_mismatch",
- operation_id,
- "account",
- message,
- CliExitCode::AuthorizationFailed,
- ),
- Self::SignerUnconfigured {
- operation_id,
- message,
- } => runtime_output_error(
- "signer_unconfigured",
- operation_id,
- "signer",
- message,
- CliExitCode::SignerUnavailable,
- ),
- Self::SignerUnavailable {
- operation_id,
- message,
- } => runtime_output_error(
- "signer_unavailable",
- operation_id,
- "signer",
- message,
- CliExitCode::SignerUnavailable,
- ),
- Self::SignerModeDeferred {
- operation_id,
- message,
- } => runtime_output_error(
- "signer_mode_deferred",
- operation_id,
- "signer",
- message,
- CliExitCode::SignerUnavailable,
- ),
- Self::ProviderUnconfigured {
- operation_id,
- message,
- } => runtime_output_error(
- "provider_unconfigured",
- operation_id,
- "provider",
- message,
- CliExitCode::RuntimeUnavailable,
- ),
- Self::ProviderUnavailable {
- operation_id,
- message,
- } => runtime_output_error(
- "provider_unavailable",
- operation_id,
- "provider",
- message,
- CliExitCode::RuntimeUnavailable,
- ),
- Self::OperationUnavailable {
- operation_id,
- message,
- } => runtime_output_error(
- "operation_unavailable",
- operation_id,
- "operation",
- message,
- CliExitCode::RuntimeUnavailable,
- ),
- Self::NotImplemented {
- operation_id,
- message,
- } => runtime_output_error(
- "not_implemented",
- operation_id,
- "operation",
- message,
- CliExitCode::RuntimeUnavailable,
- ),
- Self::DetailedFailure(failure) => runtime_output_error_with_detail(
- failure.code.as_str(),
- failure.operation_id.as_str(),
- failure.class.as_str(),
- failure.message.as_str(),
- failure.exit_code,
- failure.detail_json.as_str(),
- ),
- Self::UnknownOperation(operation_id) => OutputError::new(
- "unknown_operation",
- format!("unknown operation `{operation_id}`"),
- CliExitCode::InvalidInput,
- ),
- Self::RequestTypeMismatch { .. } | Self::ResultTypeMismatch { .. } => OutputError::new(
- "contract_mismatch",
- self.to_string(),
- CliExitCode::InternalError,
- ),
- Self::Serialization(message) => OutputError::new(
- "serialization_failed",
- message.clone(),
- CliExitCode::InternalError,
- ),
- Self::Runtime(message) => {
- OutputError::new("runtime_error", message.clone(), CliExitCode::InternalError)
- }
- }
- }
-}
-
-fn sdk_error_exit_code(class: ErrorClass) -> CliExitCode {
- match class {
- ErrorClass::Authorization | ErrorClass::Security => CliExitCode::AuthorizationFailed,
- ErrorClass::Validation | ErrorClass::Contract | ErrorClass::Conflict => {
- CliExitCode::InvalidInput
- }
- ErrorClass::Storage
- | ErrorClass::Resource
- | ErrorClass::Capability
- | ErrorClass::Maintenance => CliExitCode::RuntimeUnavailable,
- ErrorClass::Network | ErrorClass::Sync => CliExitCode::SyncOrNetworkFailure,
- _ => CliExitCode::InternalError,
- }
-}
-
-fn sdk_error_class_name(class: ErrorClass) -> &'static str {
- match class {
- ErrorClass::Validation => "validation",
- ErrorClass::Contract => "contract",
- ErrorClass::Storage => "storage",
- ErrorClass::Resource => "resource",
- ErrorClass::Conflict => "conflict",
- ErrorClass::Operation => "operation",
- ErrorClass::Authorization => "authorization",
- ErrorClass::Signer => "signer",
- ErrorClass::Network => "network",
- ErrorClass::Sync => "sync",
- ErrorClass::Runtime => "runtime",
- ErrorClass::Projection => "projection",
- ErrorClass::Query => "query",
- ErrorClass::Capability => "capability",
- ErrorClass::Privacy => "privacy",
- ErrorClass::Security => "security",
- ErrorClass::Maintenance => "maintenance",
- ErrorClass::Internal => "internal",
- ErrorClass::Unknown => "unknown",
- }
-}
-
-fn sdk_recovery_next_actions(
- operation_id: &str,
- recovery_actions: &[RecoveryAction],
-) -> Vec<String> {
- recovery_actions
- .iter()
- .filter_map(|action| match action {
- RecoveryAction::RetryOperationWithSameIdempotencyKey | RecoveryAction::FixRequest => {
- Some(operation_retry_action(operation_id))
- }
- RecoveryAction::InspectLocalStores => Some("radroots store inspect".to_owned()),
- RecoveryAction::ConfigureStorage => Some("radroots store init".to_owned()),
- RecoveryAction::InspectGeoNamesAsset | RecoveryAction::ConfigureGeoNamesCache => {
- Some("radroots health inspect".to_owned())
- }
- RecoveryAction::ConfigureTransportTargets => {
- Some("radroots transport config inspect".to_owned())
- }
- RecoveryAction::ConfigureSigner => Some("radroots signer status".to_owned()),
- RecoveryAction::SelectAuthorizedActor => Some("radroots account list".to_owned()),
- RecoveryAction::CompleteSignerAuthentication => {
- Some("radroots signer status".to_owned())
- }
- RecoveryAction::RetryAfterTransportFailure | RecoveryAction::RetryGeoNamesDownload => {
- Some(operation_retry_action(operation_id))
- }
- RecoveryAction::EnableRequiredFeature | RecoveryAction::RecreateClient => {
- Some("radroots health inspect".to_owned())
- }
- })
- .fold(Vec::new(), |mut actions, action| {
- if !actions.contains(&action) {
- actions.push(action);
- }
- actions
- })
-}
-
-fn operation_retry_action(operation_id: &str) -> String {
- format!("radroots {}", operation_id.replace('.', " "))
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum RuntimeFailureAvailability {
- Unconfigured,
- Unavailable,
-}
-
-fn account_runtime_failure(
- operation_id: &str,
- failure: &AccountRuntimeFailure,
-) -> OperationAdapterError {
- let message = failure.message().to_owned();
- match failure {
- AccountRuntimeFailure::Unresolved(_) => account_failure_output(
- operation_id,
- "account_unresolved",
- message,
- CliExitCode::AuthorizationFailed,
- failure.detail_json(),
- || OperationAdapterError::AccountUnresolved {
- operation_id: operation_id.to_owned(),
- message: failure.message().to_owned(),
- },
- ),
- AccountRuntimeFailure::WatchOnly(_) => account_failure_output(
- operation_id,
- "account_watch_only",
- message,
- CliExitCode::SignerUnavailable,
- failure.detail_json(),
- || OperationAdapterError::AccountWatchOnly {
- operation_id: operation_id.to_owned(),
- message: failure.message().to_owned(),
- },
- ),
- AccountRuntimeFailure::Mismatch(_) => account_failure_output(
- operation_id,
- "account_mismatch",
- message,
- CliExitCode::AuthorizationFailed,
- failure.detail_json(),
- || OperationAdapterError::AccountMismatch {
- operation_id: operation_id.to_owned(),
- message: failure.message().to_owned(),
- },
- ),
- }
-}
-
-fn account_failure_output(
- operation_id: &str,
- code: &str,
- message: String,
- exit_code: CliExitCode,
- detail_json: Option<&str>,
- default_error: impl FnOnce() -> OperationAdapterError,
-) -> OperationAdapterError {
- match detail_json {
- Some(detail_json) => OperationAdapterError::detailed_failure(
- operation_id.to_owned(),
- code.to_owned(),
- "account".to_owned(),
- message,
- exit_code,
- detail_json.to_owned(),
- ),
- None => default_error(),
- }
-}
-
-fn auth_runtime_failure(
- operation_id: &str,
- message: String,
- lowered: &str,
-) -> OperationAdapterError {
- let unauthorized = contains_any(
- lowered,
- &[
- "unauthorized",
- "forbidden",
- "permission denied",
- "invalid token",
- "bearer token rejected",
- "http 401",
- "http 403",
- "status 401",
- "status 403",
- ],
- );
- OperationAdapterError::detailed_failure(
- operation_id.to_owned(),
- if unauthorized {
- "auth_unauthorized".to_owned()
- } else {
- "auth_unavailable".to_owned()
- },
- "auth".to_owned(),
- message,
- CliExitCode::AuthorizationFailed,
- Value::Null.to_string(),
- )
-}
-
-fn classify_runtime_failure(
- operation_id: &str,
- message: String,
- availability: RuntimeFailureAvailability,
-) -> OperationAdapterError {
- let lowered = message.to_ascii_lowercase();
- if contains_any(&lowered, &["watch_only", "watch-only", "watch only"]) {
- return OperationAdapterError::AccountWatchOnly {
- operation_id: operation_id.to_owned(),
- message,
- };
- }
- if contains_any(&lowered, &["account mismatch"]) {
- return OperationAdapterError::AccountMismatch {
- operation_id: operation_id.to_owned(),
- message,
- };
- }
- if contains_any(
- &lowered,
- &[
- "no account",
- "no local account",
- "account selector",
- "account selection",
- "did not match any local account",
- "unresolved account",
- "selected account",
- ],
- ) {
- return OperationAdapterError::AccountUnresolved {
- operation_id: operation_id.to_owned(),
- message,
- };
- }
- if contains_any(
- &lowered,
- &[
- "signer",
- "sign_event",
- "remote_nip46",
- "nip46",
- "secret-backed",
- "secret backed",
- ],
- ) {
- return match availability {
- RuntimeFailureAvailability::Unconfigured => OperationAdapterError::SignerUnconfigured {
- operation_id: operation_id.to_owned(),
- message,
- },
- RuntimeFailureAvailability::Unavailable => OperationAdapterError::SignerUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- },
- };
- }
- if contains_any(&lowered, &["provider", "write-plane", "write plane", "rpc"]) {
- return match availability {
- RuntimeFailureAvailability::Unconfigured => {
- OperationAdapterError::ProviderUnconfigured {
- operation_id: operation_id.to_owned(),
- message,
- }
- }
- RuntimeFailureAvailability::Unavailable => OperationAdapterError::ProviderUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- },
- };
- }
- OperationAdapterError::OperationUnavailable {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
-
-fn contains_any(value: &str, needles: &[&str]) -> bool {
- needles.iter().any(|needle| value.contains(needle))
-}
-
-fn looks_like_auth_failure(value: &str) -> bool {
- contains_any(
- value,
- &[
- "authentication",
- "daemon execution auth",
- "authorization",
- "authorize",
- "unauthorized",
- "forbidden",
- "bearer token",
- "invalid token",
- "permission denied",
- "status 401",
- "status 403",
- "http 401",
- "http 403",
- ],
- )
-}
-
-fn looks_like_signer_failure(value: &str) -> bool {
- contains_any(
- value,
- &[
- "signer",
- "sign_event",
- "sign event",
- "signer session",
- "nip46",
- "nip-46",
- "remote_nip46",
- ],
- )
-}
-
-fn looks_like_provider_failure(value: &str) -> bool {
- contains_any(
- value,
- &[
- "provider unavailable",
- "provider unconfigured",
- "provider runtime",
- "provider failed",
- "execution provider",
- ],
- )
-}
-
-fn looks_like_operation_failure(value: &str) -> bool {
- contains_any(
- value,
- &[
- "method not found",
- "unknown method",
- "unsupported method",
- "unsupported operation",
- "operation unavailable",
- "operation disabled",
- "publish execution disabled",
- "publish.event is disabled",
- ],
- )
-}
-
-fn looks_like_not_found(value: &str) -> bool {
- contains_any(
- value,
- &[
- "not found",
- "no such file or directory",
- "path not found",
- "missing file",
- ],
- )
-}
-
-fn looks_like_validation_failure(value: &str) -> bool {
- contains_any(
- value,
- &[
- "invalid",
- "parse ",
- "parse:",
- "must not",
- "must be",
- "validation",
- "failed to import account",
- ],
- )
-}
-
-fn runtime_output_error(
- code: &str,
- operation_id: &str,
- class: &str,
- message: &str,
- exit_code: CliExitCode,
-) -> OutputError {
- let mut error = OutputError::new(code, message.to_owned(), exit_code);
- error.detail = Some(json!({
- "operation_id": operation_id,
- "class": class,
- }));
- error
-}
-
-fn runtime_output_error_with_detail(
- code: &str,
- operation_id: &str,
- class: &str,
- message: &str,
- exit_code: CliExitCode,
- detail_json: &str,
-) -> OutputError {
- let mut error = OutputError::new(code, message.to_owned(), exit_code);
- let mut detail = serde_json::from_str::<Map<String, Value>>(detail_json).unwrap_or_default();
- detail.insert(
- "operation_id".to_owned(),
- Value::from(operation_id.to_owned()),
- );
- detail.insert("class".to_owned(), Value::from(class.to_owned()));
- error.detail = Some(Value::Object(detail));
- error
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn final_sdk_error_maps_from_its_protocol_report() {
- let sdk_error = radroots_sdk::ClientBuilder::new()
- .build()
- .expect_err("missing storage must fail");
- let error = OperationAdapterError::sdk_failure("store.inspect", sdk_error);
-
- let output = error.to_output_error();
- let detail = output.detail.expect("detail");
- assert_eq!(detail["operation_id"], "store.inspect");
- assert!(detail["class"].is_string());
- }
-}
diff --git a/src/ops/exec/basket.rs b/src/ops/exec/basket.rs
@@ -1,1091 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_replica_schema::trade_product::{ITradeProductFieldsFilter, ITradeProductFindMany};
-use radroots_replica_store::{ReplicaSql, trade_product};
-use radroots_sql_core::SqlxSqliteExecutor;
-use serde::{Deserialize, Serialize};
-use serde_json::{Value, json};
-
-use crate::ops::{
- BasketCreateRequest, BasketCreateResult, BasketGetRequest, BasketGetResult,
- BasketItemAddRequest, BasketItemAddResult, BasketItemRemoveRequest, BasketItemRemoveResult,
- BasketItemUpdateRequest, BasketItemUpdateResult, BasketListRequest, BasketListResult,
- BasketQuoteRequest, BasketQuoteResult, OperationAdapterError, OperationRequest,
- OperationRequestData, OperationRequestPayload, OperationResult, OperationResultData,
- OperationService,
-};
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::trade::{TradeCandidateDraftCreateArgs, TradeCandidateDraftView};
-
-const BASKET_KIND: &str = "basket_v1";
-const BASKET_SOURCE: &str = "local baskets - local first";
-const BASKET_QUOTE_SOURCE: &str = "local baskets - deterministic quote";
-const BASKETS_DIR: &str = "baskets";
-
-static BASKET_COUNTER: AtomicU64 = AtomicU64::new(0);
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketDocument {
- version: u32,
- kind: String,
- basket: BasketState,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- quote: Option<BasketQuote>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketState {
- basket_id: String,
- created_at_unix: u64,
- updated_at_unix: u64,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- items: Vec<BasketItem>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- adjustments: Vec<BasketAdjustment>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketItem {
- item_id: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- listing: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- listing_addr: Option<String>,
- bin_id: String,
- quantity: u32,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketAdjustment {
- id: String,
- effect: String,
- amount: String,
- currency: String,
- reason: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketQuote {
- quote_id: String,
- quote_version: u32,
- trade_id: String,
- trade_file: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- economics: Option<Value>,
- ready_for_submit: bool,
- created_at_unix: u64,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- issues: Vec<BasketIssue>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct BasketIssue {
- code: String,
- field: String,
- message: String,
-}
-
-#[derive(Debug, Clone)]
-struct LoadedBasket {
- file: PathBuf,
- document: BasketDocument,
-}
-
-#[derive(Debug, Clone)]
-struct BasketProductBinState {
- primary_bin_id: Option<String>,
- verified_primary_bin_id: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-enum BasketProductResolution {
- Resolved(BasketProductBinState),
- Unresolved,
- Ambiguous(usize),
-}
-
-pub struct BasketOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> BasketOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<BasketCreateRequest> for BasketOperationService<'_> {
- type Result = BasketCreateResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketCreateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = string_input(&request, "basket_id").unwrap_or_else(next_basket_id);
- let initial_item = optional_item_from_request(&request, None)?;
- let file = basket_lookup_path(self.config, basket_id.as_str());
- if file.exists() {
- return Err(invalid_input(
- request.operation_id(),
- format!("basket `{basket_id}` already exists"),
- ));
- }
- if request.context.dry_run {
- return json_operation_result::<BasketCreateResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "file": file.display().to_string(),
- "item_count": initial_item.as_ref().map(|_| 1).unwrap_or(0),
- "actions": ["radroots basket create"],
- }));
- }
-
- let now = now_unix();
- let document = BasketDocument {
- version: 1,
- kind: BASKET_KIND.to_owned(),
- basket: BasketState {
- basket_id,
- created_at_unix: now,
- updated_at_unix: now,
- items: initial_item.into_iter().collect(),
- adjustments: Vec::new(),
- },
- quote: None,
- };
- save_basket(file.as_path(), &document)?;
- json_operation_result::<BasketCreateResult>(basket_view(
- self.config,
- &document,
- file.as_path(),
- None,
- )?)
- }
-}
-
-impl OperationService<BasketGetRequest> for BasketOperationService<'_> {
- type Result = BasketGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let lookup = required_basket_id(&request)?;
- let Some(loaded) = load_basket_optional(self.config, lookup.as_str())? else {
- return json_operation_result::<BasketGetResult>(missing_basket_view(
- self.config,
- lookup.as_str(),
- ));
- };
- json_operation_result::<BasketGetResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- None,
- )?)
- }
-}
-
-impl OperationService<BasketListRequest> for BasketOperationService<'_> {
- type Result = BasketListResult;
-
- fn execute(
- &self,
- _request: OperationRequest<BasketListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let baskets = list_basket_summaries(self.config)?;
- json_operation_result::<BasketListResult>(json!({
- "state": if baskets.is_empty() { "empty" } else { "ready" },
- "source": BASKET_SOURCE,
- "count": baskets.len(),
- "baskets": baskets,
- "actions": if baskets.is_empty() {
- vec!["radroots basket create".to_owned()]
- } else {
- Vec::new()
- },
- }))
- }
-}
-
-impl OperationService<BasketItemAddRequest> for BasketOperationService<'_> {
- type Result = BasketItemAddResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketItemAddRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let item = required_item_from_request(&request, Some(next_item_id(&loaded.document)))?;
- if request.context.dry_run {
- return json_operation_result::<BasketItemAddResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "item": item,
- "actions": ["radroots basket item add"],
- }));
- }
-
- loaded.document.basket.items.push(item);
- touch_basket(&mut loaded.document);
- loaded.document.quote = None;
- save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketItemAddResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- Some("updated"),
- )?)
- }
-}
-
-impl OperationService<BasketItemUpdateRequest> for BasketOperationService<'_> {
- type Result = BasketItemUpdateResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketItemUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let item_id = required_string(&request, "item_id")?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let Some(index) = loaded
- .document
- .basket
- .items
- .iter()
- .position(|item| item.item_id == item_id)
- else {
- return Err(invalid_input(
- request.operation_id(),
- format!("basket item `{item_id}` was not found"),
- ));
- };
-
- let updated =
- update_item_from_request(&request, loaded.document.basket.items[index].clone())?;
- if request.context.dry_run {
- return json_operation_result::<BasketItemUpdateResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "item": updated,
- "actions": ["radroots basket item update"],
- }));
- }
-
- loaded.document.basket.items[index] = updated;
- touch_basket(&mut loaded.document);
- loaded.document.quote = None;
- save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketItemUpdateResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- Some("updated"),
- )?)
- }
-}
-
-impl OperationService<BasketItemRemoveRequest> for BasketOperationService<'_> {
- type Result = BasketItemRemoveResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketItemRemoveRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let item_id = required_string(&request, "item_id")?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let Some(index) = loaded
- .document
- .basket
- .items
- .iter()
- .position(|item| item.item_id == item_id)
- else {
- return Err(invalid_input(
- request.operation_id(),
- format!("basket item `{item_id}` was not found"),
- ));
- };
-
- if request.context.dry_run {
- return json_operation_result::<BasketItemRemoveResult>(json!({
- "state": "dry_run",
- "source": BASKET_SOURCE,
- "basket_id": basket_id,
- "item_id": item_id,
- "actions": ["radroots basket item remove"],
- }));
- }
-
- loaded.document.basket.items.remove(index);
- touch_basket(&mut loaded.document);
- loaded.document.quote = None;
- save_basket(loaded.file.as_path(), &loaded.document)?;
- json_operation_result::<BasketItemRemoveResult>(basket_view(
- self.config,
- &loaded.document,
- loaded.file.as_path(),
- Some("updated"),
- )?)
- }
-}
-
-impl OperationService<BasketQuoteRequest> for BasketOperationService<'_> {
- type Result = BasketQuoteResult;
-
- fn execute(
- &self,
- request: OperationRequest<BasketQuoteRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let basket_id = required_basket_id(&request)?;
- let mut loaded =
- load_required_basket(self.config, basket_id.as_str(), request.operation_id())?;
- let issues = basket_issues(self.config, &loaded.document)?;
- if !issues.is_empty() {
- let actions = basket_actions(&loaded.document, issues.as_slice());
- return json_operation_result::<BasketQuoteResult>(json!({
- "state": "unconfigured",
- "source": BASKET_QUOTE_SOURCE,
- "basket_id": basket_id,
- "file": loaded.file.display().to_string(),
- "ready_for_quote": false,
- "issues": issues,
- "actions": actions,
- }));
- }
-
- let item = loaded
- .document
- .basket
- .items
- .first()
- .expect("validated basket has one item")
- .clone();
- if request.context.dry_run {
- let trade = crate::runtime::trade::scaffold_proposal_draft_preflight(
- self.config,
- &TradeCandidateDraftCreateArgs {
- listing: item.listing.clone(),
- listing_addr: item.listing_addr.clone(),
- bin_id: item.bin_id.clone(),
- quantity: item.quantity,
- },
- )
- .map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- return json_operation_result::<BasketQuoteResult>(json!({
- "state": "dry_run",
- "source": BASKET_QUOTE_SOURCE,
- "basket_id": basket_id,
- "file": loaded.file.display().to_string(),
- "item": item,
- "trade": trade,
- "actions": [format!("radroots basket quote {basket_id}")],
- }));
- }
-
- let trade = crate::runtime::trade::scaffold_proposal_draft(
- self.config,
- &TradeCandidateDraftCreateArgs {
- listing: item.listing.clone(),
- listing_addr: item.listing_addr.clone(),
- bin_id: item.bin_id.clone(),
- quantity: item.quantity,
- },
- )
- .map_err(|error| OperationAdapterError::runtime_failure(request.operation_id(), error))?;
- let quote_economics = serde_json::to_value(&trade.economics).ok();
- let quote = BasketQuote {
- quote_id: format!("quote_{}", trade.trade_id),
- quote_version: 1,
- trade_id: trade.trade_id.clone(),
- trade_file: trade.file.clone(),
- economics: quote_economics,
- ready_for_submit: trade.ready_for_submit,
- created_at_unix: now_unix(),
- issues: quote_issues_from_trade(&trade),
- };
- loaded.document.quote = Some(quote.clone());
- touch_basket(&mut loaded.document);
- save_basket(loaded.file.as_path(), &loaded.document)?;
-
- json_operation_result::<BasketQuoteResult>(json!({
- "state": "quoted",
- "source": BASKET_QUOTE_SOURCE,
- "basket_id": loaded.document.basket.basket_id,
- "file": loaded.file.display().to_string(),
- "quote": quote,
- "trade": trade,
- "actions": quote_actions(&trade),
- }))
- }
-}
-
-fn optional_item_from_request<P>(
- request: &OperationRequest<P>,
- item_id: Option<String>,
-) -> Result<Option<BasketItem>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- if string_input(request, "listing").is_none()
- && string_input(request, "listing_addr").is_none()
- && string_input(request, "bin_id").is_none()
- {
- return Ok(None);
- }
- required_item_from_request(request, item_id).map(Some)
-}
-
-fn required_item_from_request<P>(
- request: &OperationRequest<P>,
- item_id: Option<String>,
-) -> Result<BasketItem, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- let listing = string_input(request, "listing");
- let listing_addr = string_input(request, "listing_addr");
- if listing.is_none() && listing_addr.is_none() {
- return Err(invalid_input(
- request.operation_id(),
- "missing required `listing` or `listing_addr` input".to_owned(),
- ));
- }
- let bin_id = required_string(request, "bin_id")?;
- let quantity = quantity_input(request)?.unwrap_or(1);
- if quantity == 0 {
- return Err(invalid_input(
- request.operation_id(),
- "`quantity` must be greater than 0".to_owned(),
- ));
- }
-
- Ok(BasketItem {
- item_id: item_id
- .or_else(|| string_input(request, "item_id"))
- .unwrap_or_else(|| "item_1".to_owned()),
- listing,
- listing_addr,
- bin_id,
- quantity,
- })
-}
-
-fn update_item_from_request<P>(
- request: &OperationRequest<P>,
- mut item: BasketItem,
-) -> Result<BasketItem, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- let mut changed = false;
- if let Some(listing) = string_input(request, "listing") {
- item.listing = Some(listing);
- changed = true;
- }
- if let Some(listing_addr) = string_input(request, "listing_addr") {
- item.listing_addr = Some(listing_addr);
- changed = true;
- }
- if let Some(bin_id) = string_input(request, "bin_id") {
- item.bin_id = bin_id;
- changed = true;
- }
- if let Some(quantity) = quantity_input(request)? {
- if quantity == 0 {
- return Err(invalid_input(
- request.operation_id(),
- "`quantity` must be greater than 0".to_owned(),
- ));
- }
- item.quantity = quantity;
- changed = true;
- }
- if !changed {
- return Err(invalid_input(
- request.operation_id(),
- "no item update input was provided".to_owned(),
- ));
- }
- Ok(item)
-}
-
-fn basket_view(
- config: &RuntimeConfig,
- document: &BasketDocument,
- file: &Path,
- state: Option<&str>,
-) -> Result<Value, OperationAdapterError> {
- let issues = basket_issues(config, document)?;
- let ready_for_quote = issues.is_empty();
- let actions = basket_actions(document, issues.as_slice());
- Ok(json!({
- "state": state.unwrap_or("ready"),
- "source": BASKET_SOURCE,
- "basket_id": document.basket.basket_id,
- "file": file.display().to_string(),
- "item_count": document.basket.items.len(),
- "items": document.basket.items,
- "adjustment_count": document.basket.adjustments.len(),
- "adjustments": document.basket.adjustments,
- "quote": document.quote,
- "ready_for_quote": ready_for_quote,
- "issues": issues,
- "actions": actions,
- }))
-}
-
-fn missing_basket_view(config: &RuntimeConfig, lookup: &str) -> Value {
- json!({
- "state": "missing",
- "source": BASKET_SOURCE,
- "lookup": lookup,
- "file": basket_lookup_path(config, lookup).display().to_string(),
- "reason": format!("basket `{lookup}` was not found"),
- "actions": ["radroots basket list", "radroots basket create"],
- })
-}
-
-fn list_basket_summaries(config: &RuntimeConfig) -> Result<Vec<Value>, OperationAdapterError> {
- let dir = baskets_dir(config);
- if !dir.exists() {
- return Ok(Vec::new());
- }
-
- let mut baskets = Vec::new();
- for entry in fs::read_dir(&dir).map_err(|error| {
- OperationAdapterError::Runtime(format!("read basket directory {}: {error}", dir.display()))
- })? {
- let entry = entry.map_err(|error| {
- OperationAdapterError::Runtime(format!(
- "read basket directory {}: {error}",
- dir.display()
- ))
- })?;
- let path = entry.path();
- if path.extension().and_then(|value| value.to_str()) != Some("json") {
- continue;
- }
- let loaded = load_basket_path(path.as_path())?;
- let issues = basket_issues(config, &loaded.document)?;
- let ready_for_quote = issues.is_empty();
- baskets.push(json!({
- "basket_id": loaded.document.basket.basket_id,
- "state": if ready_for_quote { "ready" } else { "unconfigured" },
- "file": loaded.file.display().to_string(),
- "item_count": loaded.document.basket.items.len(),
- "adjustment_count": loaded.document.basket.adjustments.len(),
- "ready_for_quote": ready_for_quote,
- "issues": issues,
- "quote": loaded.document.quote,
- "updated_at_unix": loaded.document.basket.updated_at_unix,
- }));
- }
- baskets.sort_by(|left, right| {
- right["updated_at_unix"]
- .as_u64()
- .cmp(&left["updated_at_unix"].as_u64())
- .then_with(|| {
- left["basket_id"]
- .as_str()
- .unwrap_or_default()
- .cmp(right["basket_id"].as_str().unwrap_or_default())
- })
- });
- Ok(baskets)
-}
-
-fn basket_issues(
- config: &RuntimeConfig,
- document: &BasketDocument,
-) -> Result<Vec<BasketIssue>, OperationAdapterError> {
- let mut issues = Vec::new();
- if document.basket.items.is_empty() {
- issues.push(basket_issue(
- "basket_items_missing",
- "basket.items",
- "basket must contain one item before quote creation",
- ));
- }
- if document.basket.items.len() > 1 {
- issues.push(basket_issue(
- "basket_items_unsupported",
- "basket.items",
- "basket quotes support exactly one item",
- ));
- }
- for item in &document.basket.items {
- if item.listing.is_none() && item.listing_addr.is_none() {
- issues.push(basket_issue(
- "basket_item_listing_missing",
- format!("basket.items.{}.listing", item.item_id),
- "item must include listing or listing_addr",
- ));
- }
- if item.bin_id.trim().is_empty() {
- issues.push(basket_issue(
- "basket_item_bin_missing",
- format!("basket.items.{}.bin_id", item.item_id),
- "item must include bin_id",
- ));
- }
- if item.quantity == 0 {
- issues.push(basket_issue(
- "basket_item_quantity_invalid",
- format!("basket.items.{}.quantity", item.item_id),
- "item quantity must be greater than 0",
- ));
- }
- }
- if issues.is_empty() {
- issues.extend(basket_market_issues(config, document)?);
- }
- Ok(issues)
-}
-
-fn basket_market_issues(
- config: &RuntimeConfig,
- document: &BasketDocument,
-) -> Result<Vec<BasketIssue>, OperationAdapterError> {
- if !config.local.replica_store_path.exists() {
- return Ok(vec![basket_issue(
- "basket_market_replica_missing",
- "local.replica_store",
- "current local replica data is required before quote creation; run `radroots store inspect` and `radroots market pull`",
- )]);
- }
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path).map_err(|error| {
- OperationAdapterError::Runtime(format!(
- "open local replica {}: {error}",
- config.local.replica_store_path.display()
- ))
- })?;
- let mut issues = Vec::new();
- for item in &document.basket.items {
- let product = match basket_product_bin_state(config, &executor, item)? {
- BasketProductResolution::Resolved(product) => product,
- BasketProductResolution::Unresolved => {
- issues.push(basket_issue(
- "basket_item_listing_unresolved",
- basket_item_listing_field(item),
- "basket item listing is not active in the current local replica; run `radroots market pull` before quote creation",
- ));
- continue;
- }
- BasketProductResolution::Ambiguous(count) => {
- issues.push(basket_issue(
- "basket_item_listing_ambiguous",
- basket_item_listing_field(item),
- format!(
- "basket item listing matched {count} active local replica rows; choose a unique listing before quote creation"
- ),
- ));
- continue;
- }
- };
- let Some(primary_bin_id) = product.primary_bin_id.as_deref().and_then(non_empty_ref) else {
- issues.push(basket_issue(
- "listing_primary_bin_missing",
- format!("basket.items.{}.bin_id", item.item_id),
- "current local replica listing primary bin is required before quote creation",
- ));
- continue;
- };
- let Some(verified_primary_bin_id) = product
- .verified_primary_bin_id
- .as_deref()
- .and_then(non_empty_ref)
- else {
- issues.push(basket_issue(
- "listing_primary_bin_invalid",
- format!("basket.items.{}.bin_id", item.item_id),
- format!("current local replica primary bin `{primary_bin_id}` is not verified"),
- ));
- continue;
- };
- if verified_primary_bin_id != primary_bin_id {
- issues.push(basket_issue(
- "listing_primary_bin_invalid",
- format!("basket.items.{}.bin_id", item.item_id),
- format!(
- "current local replica primary bin `{primary_bin_id}` does not match verified primary bin `{verified_primary_bin_id}`"
- ),
- ));
- continue;
- }
- if item.bin_id != primary_bin_id {
- issues.push(basket_issue(
- "order_bin_unknown",
- format!("basket.items.{}.bin_id", item.item_id),
- format!(
- "basket bin `{}` is not in the current local listing bin set; expected primary bin `{primary_bin_id}`",
- item.bin_id
- ),
- ));
- }
- }
- Ok(issues)
-}
-
-fn basket_product_bin_state(
- config: &RuntimeConfig,
- executor: &SqlxSqliteExecutor,
- item: &BasketItem,
-) -> Result<BasketProductResolution, OperationAdapterError> {
- if let Some(listing_addr) = item.listing_addr.as_deref().and_then(non_empty_ref) {
- let product_rows = trade_product::find_many(
- executor,
- &ITradeProductFindMany {
- filter: Some(trade_product_listing_addr_filter(listing_addr)),
- },
- )
- .map_err(|error| {
- OperationAdapterError::Runtime(format!("resolve listing product state: {error:?}"))
- })?
- .results;
- let product = match product_rows.as_slice() {
- [] => return Ok(BasketProductResolution::Unresolved),
- [product] => product,
- rows => return Ok(BasketProductResolution::Ambiguous(rows.len())),
- };
- return Ok(BasketProductResolution::Resolved(BasketProductBinState {
- primary_bin_id: product.primary_bin_id.clone(),
- verified_primary_bin_id: product.verified_primary_bin_id.clone(),
- }));
- }
-
- let Some(listing_lookup) = item.listing.as_deref().and_then(non_empty_ref) else {
- return Ok(BasketProductResolution::Unresolved);
- };
- let lookup_executor =
- SqlxSqliteExecutor::open(&config.local.replica_store_path).map_err(|error| {
- OperationAdapterError::Runtime(format!(
- "open local replica {}: {error}",
- config.local.replica_store_path.display()
- ))
- })?;
- let rows = ReplicaSql::new(lookup_executor)
- .trade_product_lookup(listing_lookup)
- .map_err(|error| {
- OperationAdapterError::Runtime(format!("resolve listing product state: {error:?}"))
- })?;
- let product = match rows.as_slice() {
- [] => return Ok(BasketProductResolution::Unresolved),
- [product] => product,
- rows => return Ok(BasketProductResolution::Ambiguous(rows.len())),
- };
- Ok(BasketProductResolution::Resolved(BasketProductBinState {
- primary_bin_id: product.primary_bin_id.clone(),
- verified_primary_bin_id: product.verified_primary_bin_id.clone(),
- }))
-}
-
-fn basket_item_listing_field(item: &BasketItem) -> String {
- if item
- .listing_addr
- .as_deref()
- .and_then(non_empty_ref)
- .is_some()
- {
- format!("basket.items.{}.listing_addr", item.item_id)
- } else {
- format!("basket.items.{}.listing", item.item_id)
- }
-}
-
-fn basket_issue(
- code: impl Into<String>,
- field: impl Into<String>,
- message: impl Into<String>,
-) -> BasketIssue {
- BasketIssue {
- code: code.into(),
- field: field.into(),
- message: message.into(),
- }
-}
-
-fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsFilter {
- ITradeProductFieldsFilter {
- id: None,
- created_at: None,
- updated_at: None,
- key: None,
- category: None,
- title: None,
- summary: None,
- process: None,
- lot: None,
- profile: None,
- year: None,
- qty_amt: None,
- qty_amt_exact: None,
- qty_unit: None,
- qty_label: None,
- qty_avail: None,
- price_amt: None,
- price_amt_exact: None,
- price_currency: None,
- price_qty_amt: None,
- price_qty_amt_exact: None,
- price_qty_unit: None,
- listing_addr: Some(listing_addr.to_owned()),
- primary_bin_id: None,
- verified_primary_bin_id: None,
- notes: None,
- }
-}
-
-fn non_empty_ref(value: &str) -> Option<&str> {
- let value = value.trim();
- if value.is_empty() { None } else { Some(value) }
-}
-
-fn basket_actions(document: &BasketDocument, issues: &[BasketIssue]) -> Vec<String> {
- let basket_id = document.basket.basket_id.as_str();
- if document.basket.items.is_empty() {
- return vec![format!("radroots basket item add {basket_id}")];
- }
- if issues.is_empty() {
- vec![
- format!("radroots basket validate {basket_id}"),
- format!("radroots basket quote create {basket_id}"),
- ]
- } else {
- vec![format!("radroots basket get {basket_id}")]
- }
-}
-
-fn quote_actions(trade: &TradeCandidateDraftView) -> Vec<String> {
- if trade.ready_for_submit {
- vec![format!("radroots trade proposal submit {}", trade.file)]
- } else {
- let mut actions = vec![format!("radroots trade get {}", trade.trade_id)];
- actions.extend(trade.actions.iter().cloned());
- actions
- }
-}
-
-fn quote_issues_from_trade(trade: &TradeCandidateDraftView) -> Vec<BasketIssue> {
- trade
- .issues
- .iter()
- .map(|issue| BasketIssue {
- code: issue.code.clone(),
- field: issue.field.clone(),
- message: issue.message.clone(),
- })
- .collect()
-}
-
-fn load_required_basket(
- config: &RuntimeConfig,
- lookup: &str,
- operation_id: &str,
-) -> Result<LoadedBasket, OperationAdapterError> {
- load_basket_optional(config, lookup)?.ok_or_else(|| {
- invalid_input(
- operation_id,
- format!("basket `{lookup}` was not found; run `radroots basket create` first"),
- )
- })
-}
-
-fn load_basket_optional(
- config: &RuntimeConfig,
- lookup: &str,
-) -> Result<Option<LoadedBasket>, OperationAdapterError> {
- let path = basket_lookup_path(config, lookup);
- if !path.exists() {
- return Ok(None);
- }
- load_basket_path(path.as_path()).map(Some)
-}
-
-fn load_basket_path(path: &Path) -> Result<LoadedBasket, OperationAdapterError> {
- let contents = fs::read_to_string(path).map_err(|error| {
- OperationAdapterError::Runtime(format!("read basket {}: {error}", path.display()))
- })?;
- let document = serde_json::from_str::<BasketDocument>(contents.as_str()).map_err(|error| {
- OperationAdapterError::Runtime(format!("parse basket {}: {error}", path.display()))
- })?;
- Ok(LoadedBasket {
- file: path.to_path_buf(),
- document,
- })
-}
-
-fn save_basket(path: &Path, document: &BasketDocument) -> Result<(), OperationAdapterError> {
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent).map_err(|error| {
- OperationAdapterError::Runtime(format!(
- "create basket directory {}: {error}",
- parent.display()
- ))
- })?;
- }
- let contents = serde_json::to_string_pretty(document)
- .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?;
- fs::write(path, contents).map_err(|error| {
- OperationAdapterError::Runtime(format!("write basket {}: {error}", path.display()))
- })
-}
-
-fn baskets_dir(config: &RuntimeConfig) -> PathBuf {
- config.paths.app_data_root.join(BASKETS_DIR)
-}
-
-fn basket_lookup_path(config: &RuntimeConfig, lookup: &str) -> PathBuf {
- let candidate = PathBuf::from(lookup);
- if candidate.is_absolute() || lookup.contains(std::path::MAIN_SEPARATOR) {
- return candidate;
- }
- let file_name = if lookup.ends_with(".json") {
- lookup.to_owned()
- } else {
- format!("{lookup}.json")
- };
- baskets_dir(config).join(file_name)
-}
-
-fn touch_basket(document: &mut BasketDocument) {
- document.basket.updated_at_unix = now_unix();
-}
-
-fn next_item_id(document: &BasketDocument) -> String {
- for index in 1.. {
- let candidate = format!("item_{index}");
- if document
- .basket
- .items
- .iter()
- .all(|item| item.item_id != candidate)
- {
- return candidate;
- }
- }
- unreachable!("unbounded item id search should always return")
-}
-
-fn next_basket_id() -> String {
- let sequence = BASKET_COUNTER.fetch_add(1, Ordering::Relaxed) + 1;
- format!("basket_{}_{}", now_unix(), sequence)
-}
-
-fn now_unix() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or_default()
-}
-
-fn required_basket_id<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, "basket_id")
- .or_else(|| string_input(request, "key"))
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- "missing required `basket_id` input".to_owned(),
- )
- })
-}
-
-fn required_string<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn quantity_input<P>(request: &OperationRequest<P>) -> Result<Option<u32>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- let value = request
- .payload
- .input()
- .get("quantity")
- .or_else(|| request.payload.input().get("bin_count"));
- let Some(value) = value else {
- return Ok(None);
- };
- match value {
- Value::Number(number) => number
- .as_u64()
- .and_then(|value| u32::try_from(value).ok())
- .map(Some)
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- "`quantity` input must fit in u32".to_owned(),
- )
- }),
- Value::String(value) => value.parse::<u32>().map(Some).map_err(|error| {
- invalid_input(
- request.operation_id(),
- format!("`quantity` input must be a u32: {error}"),
- )
- }),
- _ => Err(invalid_input(
- request.operation_id(),
- "`quantity` input must be a number or string".to_owned(),
- )),
- }
-}
-
-fn json_operation_result<R>(value: Value) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- OperationResult::new(R::from_value(value))
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
diff --git a/src/ops/exec/core.rs b/src/ops/exec/core.rs
@@ -1,792 +0,0 @@
-use std::path::PathBuf;
-
-use crate::runtime::config::RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE;
-use serde::Serialize;
-use serde_json::{Value, json};
-
-use crate::ops::{
- AccountCreateRequest, AccountCreateResult, AccountImportRequest, AccountImportResult,
- AccountListRequest, AccountListResult, AccountRemoveRequest, AccountRemoveResult,
- AccountSelectRequest, AccountSelectResult, HealthInspectRequest, HealthInspectResult,
- OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
- OperationResult, OperationResultData, OperationService, ProfileInspectRequest,
- ProfileInspectResult, ProfileResetRequest, ProfileResetResult, StoreBackupRequest,
- StoreBackupResult, StoreInspectRequest, StoreInspectResult, StoreRestoreRequest,
- StoreRestoreResult,
-};
-use crate::out::envelope::OutputWarning;
-use crate::runtime::RuntimeError;
-use crate::runtime::account::{
- AccountResolution, AccountRuntimeFailure, account_resolution_view, account_summary_view,
- create_default_account, import_public_identity, preview_account_removal,
- preview_public_identity_import, remove_account, resolve_account_resolution,
- resolve_account_selector, secret_backend_status, select_account, snapshot,
-};
-use crate::runtime::config::{RuntimeConfig, SignerBackend, TransportProfileKind};
-use crate::runtime::logging::LoggingState;
-use crate::runtime::sdk::CliSdkAdapterError;
-use crate::runtime::signer::resolve_signer_status;
-use crate::view::runtime::{
- CommandDisposition, LocalBackupView, LocalRestoreView, PublishProviderRuntimeView,
- PublishRelayRuntimeView, PublishRuntimeView,
-};
-
-pub struct CoreOperationService<'a> {
- config: &'a RuntimeConfig,
- logging: &'a LoggingState,
-}
-
-impl<'a> CoreOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig, logging: &'a LoggingState) -> Self {
- Self { config, logging }
- }
-}
-
-impl OperationService<ProfileResetRequest> for CoreOperationService<'_> {
- type Result = ProfileResetResult;
-
- fn execute(
- &self,
- request: OperationRequest<ProfileResetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.dry_run {
- let local = map_runtime(crate::runtime::store::init_preflight(self.config))?;
- return json_operation_result::<ProfileResetResult>(json!({
- "state": local.state,
- "profile": self.config.paths.profile,
- "local": local,
- }));
- }
-
- let local = map_runtime(crate::runtime::store::init(self.config))?;
- json_operation_result::<ProfileResetResult>(json!({
- "state": local.state,
- "profile": self.config.paths.profile,
- "local": local,
- }))
- }
-}
-
-impl OperationService<ProfileInspectRequest> for CoreOperationService<'_> {
- type Result = ProfileInspectResult;
-
- fn execute(
- &self,
- _request: OperationRequest<ProfileInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- json_operation_result::<ProfileInspectResult>(json!({
- "profile": self.config.paths.profile,
- "profile_source": self.config.paths.profile_source,
- "root_source": self.config.paths.root_source,
- "app_namespace": self.config.paths.app_namespace,
- "workspace_config_path": self.config.paths.workspace_config_path.as_ref().map(|path| path.display().to_string()),
- "app_config_path": self.config.paths.app_config_path.display().to_string(),
- "app_data_root": self.config.paths.app_data_root.display().to_string(),
- "shared_cache_root": self.config.paths.shared_cache_root.display().to_string(),
- "app_logs_root": self.config.paths.app_logs_root.display().to_string(),
- "local_root": self.config.local.root.display().to_string(),
- "replica_store_path": self.config.local.replica_store_path.display().to_string(),
- }))
- }
-}
-
-impl OperationService<HealthInspectRequest> for CoreOperationService<'_> {
- type Result = HealthInspectResult;
-
- fn execute(
- &self,
- request: OperationRequest<HealthInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let store = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::status(self.config),
- )?;
- let account = map_runtime(resolve_account_resolution(self.config))?;
- let publish = publish_runtime_view(self.config, true, &account);
- let signer = signer_health_view(self.config, &account);
- let state = health_status_state(&store.state, &publish);
- let actions = health_actions(self.config, store.state.as_str(), &account, &publish);
- json_operation_result::<HealthInspectResult>(json!({
- "state": state,
- "store": store,
- "account_resolution": account_resolution_view(&account),
- "signer": signer,
- "publish": publish,
- "logging": {
- "initialized": self.logging.initialized,
- "current_file": self.logging.current_file.as_ref().map(|path| path.display().to_string()),
- },
- "actions": actions,
- }))
- }
-}
-
-impl OperationService<AccountCreateRequest> for CoreOperationService<'_> {
- type Result = AccountCreateResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountCreateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if request.context.dry_run {
- let secret_backend = secret_backend_status(self.config);
- if secret_backend.state != "ready" {
- return Err(OperationAdapterError::OperationUnavailable {
- operation_id: request.operation_id().to_owned(),
- message: secret_backend
- .reason
- .unwrap_or_else(|| "account secret backend is not available".to_owned()),
- });
- }
- return json_operation_result::<AccountCreateResult>(json!({
- "state": "dry_run",
- "store_path": self.config.account.store_path.display().to_string(),
- "secrets_dir": self.config.account.secrets_dir.display().to_string(),
- "secret_backend": {
- "state": secret_backend.state,
- "active_backend": secret_backend.active_backend,
- },
- }));
- }
-
- let result = map_runtime(create_default_account(self.config))?;
- json_operation_result::<AccountCreateResult>(json!({
- "state": "created",
- "account": account_summary_view(&result.account),
- }))
- }
-}
-
-impl OperationService<AccountImportRequest> for CoreOperationService<'_> {
- type Result = AccountImportResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountImportRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let path = required_path(&request, "path")?;
- let make_default = bool_input(&request, "default").unwrap_or(false);
- if request.context.dry_run {
- let account = map_expected_runtime(
- request.operation_id(),
- preview_public_identity_import(self.config, path.as_path(), make_default),
- )?;
- return json_operation_result::<AccountImportResult>(json!({
- "state": "dry_run",
- "path": path.display().to_string(),
- "default": make_default,
- "account": account_summary_view(&account),
- }));
- }
-
- let account = map_expected_runtime(
- request.operation_id(),
- import_public_identity(self.config, path.as_path(), make_default),
- )?;
- json_operation_result::<AccountImportResult>(json!({
- "state": "imported",
- "account": account_summary_view(&account),
- }))
- }
-}
-
-impl OperationService<AccountListRequest> for CoreOperationService<'_> {
- type Result = AccountListResult;
-
- fn execute(
- &self,
- _request: OperationRequest<AccountListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let snapshot = map_runtime(snapshot(self.config))?;
- let accounts = snapshot
- .accounts
- .iter()
- .map(account_summary_view)
- .collect::<Vec<_>>();
- json_operation_result::<AccountListResult>(json!({
- "source": crate::runtime::account::SHARED_ACCOUNT_STORE_SOURCE,
- "count": accounts.len(),
- "accounts": accounts,
- }))
- }
-}
-
-impl OperationService<AccountRemoveRequest> for CoreOperationService<'_> {
- type Result = AccountRemoveResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountRemoveRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let selector = required_string(&request, "selector")?;
- if request.context.dry_run {
- let preview =
- preview_account_removal(self.config, selector.as_str()).map_err(|error| {
- OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
- })?;
- return json_operation_result::<AccountRemoveResult>(json!({
- "state": "dry_run",
- "removed_account": account_summary_view(&preview.account),
- "default_would_clear": preview.default_would_clear,
- "remaining_account_count": preview.remaining_account_count,
- }));
- }
-
- let resolved_farm_config =
- map_runtime(crate::runtime::farm_config::load(self.config, None))?;
- let result = remove_account(self.config, selector.as_str()).map_err(|error| {
- OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
- })?;
- let removed_account_id = result.removed_account.record.id().to_string();
- let farm_orphan_warning =
- account_remove_farm_orphan_warning(resolved_farm_config.as_ref(), &removed_account_id);
- let mut result_value = json!({
- "state": "removed",
- "removed_account": account_summary_view(&result.removed_account),
- "default_cleared": result.default_cleared,
- "remaining_account_count": result.remaining_account_count,
- });
- if let Some(warning) = farm_orphan_warning.as_ref() {
- result_value["warnings"] = json!([warning.result_value()]);
- result_value["actions"] = json!(warning.actions.clone());
- }
- let mut operation_result = json_operation_result::<AccountRemoveResult>(result_value)?;
- if let Some(warning) = farm_orphan_warning {
- operation_result.warnings.push(warning.output_warning());
- }
- Ok(operation_result)
- }
-}
-
-impl OperationService<AccountSelectRequest> for CoreOperationService<'_> {
- type Result = AccountSelectResult;
-
- fn execute(
- &self,
- request: OperationRequest<AccountSelectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let selector = required_string(&request, "selector")?;
- if request.context.dry_run {
- let account =
- resolve_account_selector(self.config, selector.as_str()).map_err(|error| {
- OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
- })?;
- return json_operation_result::<AccountSelectResult>(json!({
- "state": "dry_run",
- "account": account_summary_view(&account),
- }));
- }
-
- let account = select_account(self.config, selector.as_str()).map_err(|error| {
- OperationAdapterError::unconfigured(request.operation_id(), error.to_string())
- })?;
- json_operation_result::<AccountSelectResult>(json!({
- "state": "default",
- "account": account_summary_view(&account),
- }))
- }
-}
-
-impl OperationService<StoreInspectRequest> for CoreOperationService<'_> {
- type Result = StoreInspectResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::status(self.config),
- )?;
- serialized_operation_result::<StoreInspectResult, _>(&view)
- }
-}
-
-impl OperationService<StoreBackupRequest> for CoreOperationService<'_> {
- type Result = StoreBackupResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreBackupRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let output = optional_path(&request, "output")
- .unwrap_or_else(|| self.config.local.backups_dir.join("sdk-store-backup"));
- if request.context.dry_run {
- let view = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::backup_preflight(self.config, output.as_path()),
- )?;
- return local_backup_result(request.operation_id(), &view);
- }
-
- let view = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::backup(self.config, output.as_path()),
- )?;
- local_backup_result(request.operation_id(), &view)
- }
-}
-
-impl OperationService<StoreRestoreRequest> for CoreOperationService<'_> {
- type Result = StoreRestoreResult;
-
- fn execute(
- &self,
- request: OperationRequest<StoreRestoreRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let source = required_path(&request, "source")?;
- let destination = optional_path(&request, "destination");
- let overwrite = bool_input(&request, "overwrite").unwrap_or(false);
-
- let view = map_sdk_adapter(
- request.operation_id(),
- crate::runtime::store::restore(
- self.config,
- source.as_path(),
- destination.as_deref(),
- overwrite,
- request.context.dry_run,
- ),
- )?;
- local_restore_result(request.operation_id(), &view)
- }
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn json_operation_result<R>(value: Value) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- OperationResult::new(R::from_value(value))
-}
-
-fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::Runtime(error.to_string()))
-}
-
-fn map_sdk_adapter<T>(
- operation_id: &str,
- result: Result<T, CliSdkAdapterError>,
-) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::sdk_adapter_failure(operation_id, error))
-}
-
-fn map_expected_runtime<T>(
- operation_id: &str,
- result: Result<T, RuntimeError>,
-) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
-}
-
-fn local_backup_result(
- operation_id: &str,
- view: &LocalBackupView,
-) -> Result<OperationResult<StoreBackupResult>, OperationAdapterError> {
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<StoreBackupResult, _>(view),
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| match disposition {
- CommandDisposition::Success => "store backup succeeded".to_owned(),
- CommandDisposition::NotFound => "store backup target was not found".to_owned(),
- CommandDisposition::ValidationFailed => "store backup validation failed".to_owned(),
- CommandDisposition::Unconfigured => "store backup is unconfigured".to_owned(),
- CommandDisposition::ExternalUnavailable => "store backup is unavailable".to_owned(),
- CommandDisposition::Unsupported => "store backup is unsupported".to_owned(),
- CommandDisposition::InternalError => "store backup failed".to_owned(),
- }),
- )),
- }
-}
-
-fn local_restore_result(
- operation_id: &str,
- view: &LocalRestoreView,
-) -> Result<OperationResult<StoreRestoreResult>, OperationAdapterError> {
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<StoreRestoreResult, _>(view),
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| match disposition {
- CommandDisposition::Success => "store restore succeeded".to_owned(),
- CommandDisposition::NotFound => "store restore source was not found".to_owned(),
- CommandDisposition::ValidationFailed => {
- "store restore validation failed".to_owned()
- }
- CommandDisposition::Unconfigured => "store restore is unconfigured".to_owned(),
- CommandDisposition::ExternalUnavailable => {
- "store restore is unavailable".to_owned()
- }
- CommandDisposition::Unsupported => "store restore is unsupported".to_owned(),
- CommandDisposition::InternalError => "store restore failed".to_owned(),
- }),
- )),
- }
-}
-
-fn publish_runtime_view(
- config: &RuntimeConfig,
- signed_write_required: bool,
- account: &AccountResolution,
-) -> PublishRuntimeView {
- let relay_ready = !config.transport.nostr_relay_urls.is_empty();
- let source = config.transport.source.as_str().to_owned();
- let relay = PublishRelayRuntimeView {
- ready: relay_ready,
- count: config.transport.nostr_relay_urls.len(),
- source: config.transport.source.as_str().to_owned(),
- };
-
- match config.transport.profile {
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => {
- let (state, executable, reason) =
- nostr_publish_readiness(config, relay_ready, signed_write_required, account);
- PublishRuntimeView {
- transport: config.transport.profile.as_str().to_owned(),
- source,
- transport_family: config.transport.profile.transport_family().to_owned(),
- state: state.to_owned(),
- executable,
- reason: reason.clone(),
- signed_write_required,
- relay,
- provider: PublishProviderRuntimeView {
- provider_runtime_id: config.transport.profile.as_str().to_owned(),
- state: state.to_owned(),
- source: config.transport.source.as_str().to_owned(),
- reason,
- },
- }
- }
- TransportProfileKind::LocalOnly => PublishRuntimeView {
- transport: config.transport.profile.as_str().to_owned(),
- source,
- transport_family: config.transport.profile.transport_family().to_owned(),
- state: "local_only".to_owned(),
- executable: false,
- reason: Some(
- "local_only transport profile does not perform network publish".to_owned(),
- ),
- signed_write_required,
- relay,
- provider: PublishProviderRuntimeView {
- provider_runtime_id: "local_only".to_owned(),
- state: "local_only".to_owned(),
- source: config.transport.source.as_str().to_owned(),
- reason: Some("local_only transport profile writes only to local state".to_owned()),
- },
- },
- TransportProfileKind::Reticulum => PublishRuntimeView {
- transport: config.transport.profile.as_str().to_owned(),
- source,
- transport_family: config.transport.profile.transport_family().to_owned(),
- state: "unavailable".to_owned(),
- executable: false,
- reason: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()),
- signed_write_required,
- relay,
- provider: PublishProviderRuntimeView {
- provider_runtime_id: "reticulum".to_owned(),
- state: "unavailable".to_owned(),
- source: config.transport.source.as_str().to_owned(),
- reason: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()),
- },
- },
- }
-}
-
-fn nostr_publish_readiness(
- config: &RuntimeConfig,
- relay_ready: bool,
- signed_write_required: bool,
- account: &AccountResolution,
-) -> (&'static str, bool, Option<String>) {
- if !relay_ready {
- return (
- "unconfigured",
- false,
- Some(
- "Nostr transport profile requires at least one configured Nostr relay for writes"
- .to_owned(),
- ),
- );
- }
-
- if !signed_write_required {
- return ("ready", true, None);
- }
-
- if matches!(config.signer.backend, SignerBackend::Myc) {
- let signer = resolve_signer_status(config);
- return if signer.state == "ready" {
- ("ready", true, None)
- } else {
- ("unconfigured", false, signer.reason)
- };
- }
-
- let Some(resolved_account) = account.resolved_account.as_ref() else {
- return (
- "unconfigured",
- false,
- Some(
- "Nostr transport profile requires a selected or default write-capable local account for signed writes"
- .to_owned(),
- ),
- );
- };
-
- if !resolved_account.write_capable {
- return (
- "unconfigured",
- false,
- Some(AccountRuntimeFailure::watch_only(&resolved_account.record.id()).to_string()),
- );
- }
-
- ("ready", true, None)
-}
-
-fn signer_health_view(config: &RuntimeConfig, account: &AccountResolution) -> Value {
- match config.signer.backend {
- SignerBackend::Local => {
- let write_capable = account
- .resolved_account
- .as_ref()
- .map(|account| account.write_capable)
- .unwrap_or(false);
- json!({
- "state": if write_capable { "ready" } else { "unconfigured" },
- "backend": config.signer.backend.as_str(),
- "write_capable_account": write_capable,
- "reason": if write_capable {
- Value::Null
- } else {
- json!("local signer requires a selected or default write-capable local account")
- },
- })
- }
- SignerBackend::Myc => {
- let signer = resolve_signer_status(config);
- json!({
- "state": signer.state,
- "backend": config.signer.backend.as_str(),
- "write_capable_account": signer.reason.is_none(),
- "reason": signer.reason,
- "binding_state": signer.binding.state,
- })
- }
- }
-}
-
-fn health_status_state(store_state: &str, publish: &PublishRuntimeView) -> &'static str {
- if store_state == "ready" && publish_runtime_ready(publish) {
- "ready"
- } else {
- "needs_attention"
- }
-}
-
-fn publish_runtime_ready(publish: &PublishRuntimeView) -> bool {
- !publish.signed_write_required || publish.executable
-}
-
-fn health_actions(
- config: &RuntimeConfig,
- store_state: &str,
- account: &AccountResolution,
- publish: &PublishRuntimeView,
-) -> Vec<String> {
- let mut actions = Vec::new();
- if store_state != "ready" {
- push_unique(&mut actions, "radroots store inspect");
- }
- if let Some(resolved) = account.resolved_account.as_ref() {
- if !resolved.write_capable {
- push_unique(&mut actions, "radroots account create");
- }
- } else {
- push_unique(&mut actions, "radroots account create");
- }
- for action in publish_recovery_actions(config, account, publish) {
- push_unique(&mut actions, action);
- }
- actions
-}
-
-#[derive(Debug, Clone)]
-struct AccountRemoveFarmOrphanWarning {
- message: String,
- subject_account_id: String,
- farm_config_scope: String,
- farm_config_path: String,
- actions: Vec<String>,
-}
-
-impl AccountRemoveFarmOrphanWarning {
- const CODE: &'static str = "farm_bound_seller_orphaned";
-
- fn result_value(&self) -> Value {
- json!({
- "code": Self::CODE,
- "message": self.message.clone(),
- "subject_account_id": self.subject_account_id.clone(),
- "farm_config": {
- "scope": self.farm_config_scope.clone(),
- "path": self.farm_config_path.clone(),
- },
- "actions": self.actions.clone(),
- })
- }
-
- fn output_warning(&self) -> OutputWarning {
- OutputWarning {
- code: Self::CODE.to_owned(),
- message: self.message.clone(),
- }
- }
-}
-
-fn account_remove_farm_orphan_warning(
- resolved: Option<&crate::runtime::farm_config::ResolvedFarmConfig>,
- removed_account_id: &str,
-) -> Option<AccountRemoveFarmOrphanWarning> {
- let resolved = resolved?;
- if resolved.document.selection.account != removed_account_id {
- return None;
- }
- Some(AccountRemoveFarmOrphanWarning {
- message: format!(
- "removed account `{removed_account_id}` is still bound as the farm seller account"
- ),
- subject_account_id: removed_account_id.to_owned(),
- farm_config_scope: resolved.scope.as_str().to_owned(),
- farm_config_path: resolved.path.display().to_string(),
- actions: crate::runtime::farm::farm_bound_seller_recovery_actions("<selector>"),
- })
-}
-
-fn publish_recovery_actions(
- config: &RuntimeConfig,
- account: &AccountResolution,
- publish: &PublishRuntimeView,
-) -> Vec<String> {
- if publish.state == "ready" {
- return Vec::new();
- }
-
- let mut actions = Vec::new();
- match config.transport.profile {
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => {
- if config.transport.nostr_relay_urls.is_empty() {
- push_unique(
- &mut actions,
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
- );
- }
- if publish.signed_write_required {
- if matches!(config.signer.backend, SignerBackend::Myc) {
- push_unique(&mut actions, "radroots signer status");
- } else if let Some(resolved) = account.resolved_account.as_ref() {
- if !resolved.write_capable {
- push_unique(&mut actions, "radroots account create");
- }
- } else {
- push_unique(&mut actions, "radroots account create");
- }
- }
- }
- TransportProfileKind::LocalOnly => {
- push_unique(
- &mut actions,
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
- );
- }
- TransportProfileKind::Reticulum => {
- push_unique(
- &mut actions,
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com",
- );
- }
- }
- actions
-}
-
-fn push_unique(actions: &mut Vec<String>, action: impl Into<String>) {
- let action = action.into();
- if !actions.contains(&action) {
- actions.push(action);
- }
-}
-
-fn required_string<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn required_path<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<PathBuf, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- optional_path(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn optional_path<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).map(PathBuf::from)
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request.payload.input().get(key).and_then(Value::as_bool)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs
@@ -1,303 +0,0 @@
-use serde::Serialize;
-use serde_json::Value;
-
-use crate::cli::global::{
- FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs,
-};
-use crate::ops::{
- FarmCreateRequest, FarmCreateResult, FarmGetRequest, FarmGetResult, FarmListRequest,
- FarmListResult, FarmPublishRequest, FarmPublishResult, FarmUpdateRequest, FarmUpdateResult,
- OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
- OperationResult, OperationResultData, OperationService,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
-use crate::view::runtime::{CommandDisposition, FarmPublishView};
-
-pub struct FarmOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> FarmOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<FarmCreateRequest> for FarmOperationService<'_> {
- type Result = FarmCreateResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmCreateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmCreateArgs {
- scope: scope_input(&request)?,
- farm_d_tag: string_input(&request, "farm_d_tag"),
- name: string_input(&request, "name"),
- display_name: string_input(&request, "display_name"),
- about: string_input(&request, "about"),
- website: string_input(&request, "website"),
- picture: string_input(&request, "picture"),
- banner: string_input(&request, "banner"),
- location: string_input(&request, "location"),
- city: string_input(&request, "city"),
- region: string_input(&request, "region"),
- country: string_input(&request, "country"),
- geohash: string_input(&request, "geohash"),
- delivery_method: string_input(&request, "delivery_method"),
- };
- if request.context.dry_run {
- let view =
- crate::runtime::farm::init_preflight(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- return serialized_operation_result::<FarmCreateResult, _>(&view);
- }
-
- let view = crate::runtime::farm::init(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
- })?;
- serialized_operation_result::<FarmCreateResult, _>(&view)
- }
-}
-
-impl OperationService<FarmGetRequest> for FarmOperationService<'_> {
- type Result = FarmGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmScopedArgs {
- scope: scope_input(&request)?,
- };
- let view = map_runtime(crate::runtime::farm::get(self.config, &args))?;
- serialized_operation_result::<FarmGetResult, _>(&view)
- }
-}
-
-impl OperationService<FarmUpdateRequest> for FarmOperationService<'_> {
- type Result = FarmUpdateResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- farm_set::<FarmUpdateResult>(&request, self.config, profile_field(&request)?)
- }
-}
-
-impl OperationService<FarmListRequest> for FarmOperationService<'_> {
- type Result = FarmListResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmScopedArgs {
- scope: scope_input(&request)?,
- };
- let view = map_runtime(crate::runtime::farm::status(self.config, &args))?;
- serialized_operation_result::<FarmListResult, _>(&view)
- }
-}
-
-impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
- type Result = FarmPublishResult;
-
- fn execute(
- &self,
- request: OperationRequest<FarmPublishRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FarmPublishArgs {
- scope: scope_input(&request)?,
- idempotency_key: request
- .context
- .idempotency_key
- .clone()
- .or_else(|| string_input(&request, "idempotency_key")),
- print_event: bool_input(&request, "print_event").unwrap_or(false),
- };
- if matches!(
- self.config.transport.profile,
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
- ) {
- require_nostr_delivery_target(&request, self.config)?;
- }
-
- let view = crate::runtime::farm::publish(self.config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- farm_publish_result(request.operation_id(), &view)
- }
-}
-
-fn farm_set<R>(
- request: &OperationRequest<impl OperationRequestData>,
- config: &RuntimeConfig,
- field: FarmFieldArg,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- let value = required_string(request, "value")?;
- let args = FarmUpdateArgs {
- scope: scope_input(request)?,
- field,
- value: vec![value.clone()],
- };
- if request.context.dry_run {
- let view = map_runtime(crate::runtime::farm::set_preflight(config, &args))?;
- return serialized_operation_result::<R, _>(&view);
- }
-
- let view = map_runtime(crate::runtime::farm::set(config, &args))?;
- serialized_operation_result::<R, _>(&view)
-}
-
-fn profile_field(
- request: &OperationRequest<impl OperationRequestData>,
-) -> Result<FarmFieldArg, OperationAdapterError> {
- match string_input(request, "field").as_deref() {
- Some("name") | None => Ok(FarmFieldArg::Name),
- Some("display_name") | Some("display-name") => Ok(FarmFieldArg::DisplayName),
- Some("about") => Ok(FarmFieldArg::About),
- Some("website") => Ok(FarmFieldArg::Website),
- Some("picture") => Ok(FarmFieldArg::Picture),
- Some("banner") => Ok(FarmFieldArg::Banner),
- Some(other) => Err(invalid_input(
- request.operation_id(),
- format!("profile field `{other}` is not supported"),
- )),
- }
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn farm_publish_result(
- operation_id: &str,
- view: &FarmPublishView,
-) -> Result<OperationResult<FarmPublishResult>, OperationAdapterError> {
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<FarmPublishResult, _>(view),
- CommandDisposition::ExternalUnavailable
- if farm_publish_transport_delivery_unavailable(view) =>
- {
- Err(OperationAdapterError::network_unavailable_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!("farm publish finished with state `{}`", view.state)
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- ))
- }
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| match disposition {
- CommandDisposition::Success => "farm publish succeeded".to_owned(),
- CommandDisposition::NotFound => "farm publish target was not found".to_owned(),
- CommandDisposition::ValidationFailed => "farm publish validation failed".to_owned(),
- CommandDisposition::Unconfigured => "farm publish is unconfigured".to_owned(),
- CommandDisposition::ExternalUnavailable => "farm publish is unavailable".to_owned(),
- CommandDisposition::Unsupported => "farm publish is unsupported".to_owned(),
- CommandDisposition::InternalError => "farm publish failed".to_owned(),
- }),
- )),
- }
-}
-
-fn farm_publish_transport_delivery_unavailable(view: &FarmPublishView) -> bool {
- view.state == "partial"
- || !view.profile.failed_transport_targets.is_empty()
- || !view.farm.failed_transport_targets.is_empty()
-}
-
-fn require_nostr_delivery_target<P>(
- request: &OperationRequest<P>,
- config: &RuntimeConfig,
-) -> Result<(), OperationAdapterError>
-where
- P: OperationRequestPayload,
-{
- if !config.transport.nostr_relay_urls.is_empty() {
- return Ok(());
- }
-
- Err(OperationAdapterError::NetworkUnavailable {
- operation_id: request.operation_id().to_owned(),
- message: format!(
- "`{}` requires at least one configured Nostr relay in the active transport profile",
- request.spec.cli_path
- ),
- })
-}
-
-fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::Runtime(error.to_string()))
-}
-
-fn scope_input<P>(
- request: &OperationRequest<P>,
-) -> Result<Option<FarmScopeArg>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- match string_input(request, "scope").as_deref() {
- Some("user") => Ok(Some(FarmScopeArg::User)),
- Some("workspace") => Ok(Some(FarmScopeArg::Workspace)),
- Some(other) => Err(invalid_input(
- request.operation_id(),
- format!("scope must be `user` or `workspace`, got `{other}`"),
- )),
- None => Ok(None),
- }
-}
-
-fn required_string<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request.payload.input().get(key).and_then(Value::as_bool)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
diff --git a/src/ops/exec/listing.rs b/src/ops/exec/listing.rs
@@ -1,507 +0,0 @@
-use std::path::PathBuf;
-
-use serde::Serialize;
-use serde_json::Value;
-
-use crate::cli::global::{ListingCreateArgs, ListingMutationArgs, RecordLookupArgs};
-use crate::ops::{
- ListingCreateRequest, ListingCreateResult, ListingGetRequest, ListingGetResult,
- ListingListRequest, ListingListResult, ListingPauseRequest, ListingPauseResult,
- ListingPublishRequest, ListingPublishResult, ListingUpdateRequest, ListingUpdateResult,
- ListingWithdrawRequest, ListingWithdrawResult, OperationAdapterError, OperationNetworkMode,
- OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
- OperationResultData, OperationService,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{CommandDisposition, ListingMutationView};
-
-pub struct ListingOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> ListingOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<ListingCreateRequest> for ListingOperationService<'_> {
- type Result = ListingCreateResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingCreateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = ListingCreateArgs {
- output: optional_path(&request, "output"),
- key: string_input(&request, "key"),
- title: string_input(&request, "title"),
- category: string_input(&request, "category"),
- summary: string_input(&request, "summary"),
- bin_id: string_input(&request, "bin_id"),
- quantity_amount: string_input(&request, "quantity_amount"),
- quantity_unit: string_input(&request, "quantity_unit"),
- price_amount: string_input(&request, "price_amount"),
- price_currency: string_input(&request, "price_currency"),
- price_per_amount: string_input(&request, "price_per_amount"),
- price_per_unit: string_input(&request, "price_per_unit"),
- available: string_input(&request, "available"),
- label: string_input(&request, "label"),
- discount_id: string_input(&request, "discount_id"),
- discount_label: string_input(&request, "discount_label"),
- discount_kind: string_input(&request, "discount_kind"),
- discount_value: string_input(&request, "discount_value"),
- discount_amount: string_input(&request, "discount_amount"),
- discount_currency: string_input(&request, "discount_currency"),
- };
- if request.context.dry_run {
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::scaffold_preflight(self.config, &args),
- )?;
- return serialized_operation_result::<ListingCreateResult, _>(&view);
- }
-
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::scaffold(self.config, &args),
- )?;
- serialized_operation_result::<ListingCreateResult, _>(&view)
- }
-}
-
-impl OperationService<ListingGetRequest> for ListingOperationService<'_> {
- type Result = ListingGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = RecordLookupArgs {
- key: required_string(&request, "key")?,
- };
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::get(self.config, &args),
- )?;
- serialized_operation_result::<ListingGetResult, _>(&view)
- }
-}
-
-impl OperationService<ListingListRequest> for ListingOperationService<'_> {
- type Result = ListingListResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::listing::list(self.config),
- )?;
- serialized_operation_result::<ListingListResult, _>(&view)
- }
-}
-
-impl OperationService<ListingUpdateRequest> for ListingOperationService<'_> {
- type Result = ListingUpdateResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = mutation_args(&request)?;
- let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::update(&config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- mutation_result::<ListingUpdateResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<ListingPublishRequest> for ListingOperationService<'_> {
- type Result = ListingPublishResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingPublishRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = mutation_args(&request)?;
- let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::publish_via_sdk(&config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- mutation_result::<ListingPublishResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<ListingPauseRequest> for ListingOperationService<'_> {
- type Result = ListingPauseResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingPauseRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = mutation_args(&request)?;
- let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::pause(&config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- mutation_result::<ListingPauseResult>(request.operation_id(), &view)
- }
-}
-
-impl OperationService<ListingWithdrawRequest> for ListingOperationService<'_> {
- type Result = ListingWithdrawResult;
-
- fn execute(
- &self,
- request: OperationRequest<ListingWithdrawRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = mutation_args(&request)?;
- let config = mutation_config(self.config, &request);
- let view = crate::runtime::listing::withdraw(&config, &args).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- mutation_result::<ListingWithdrawResult>(request.operation_id(), &view)
- }
-}
-
-fn mutation_config<P>(config: &RuntimeConfig, request: &OperationRequest<P>) -> RuntimeConfig
-where
- P: OperationRequestPayload,
-{
- let mut config = config.clone();
- if request.context.dry_run {
- config.output.dry_run = true;
- }
- config
-}
-
-fn mutation_args<P>(
- request: &OperationRequest<P>,
-) -> Result<ListingMutationArgs, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- Ok(ListingMutationArgs {
- file: required_path(request, "file")?,
- idempotency_key: request
- .context
- .idempotency_key
- .clone()
- .or_else(|| string_input(request, "idempotency_key")),
- print_event: bool_input(request, "print_event").unwrap_or(false),
- offline: matches!(request.context.network_mode, OperationNetworkMode::Offline),
- })
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn mutation_result<R>(
- operation_id: &str,
- view: &ListingMutationView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<R, _>(view),
- CommandDisposition::ExternalUnavailable if listing_transport_delivery_unavailable(view) => {
- Err(OperationAdapterError::network_unavailable_with_detail(
- operation_id,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "listing {} finished with state `{}`",
- view.operation, view.state
- )
- }),
- serde_json::to_value(view).unwrap_or(Value::Null),
- ))
- }
- disposition => Err(OperationAdapterError::from_command_disposition(
- operation_id,
- disposition,
- view.reason.clone().unwrap_or_else(|| {
- format!(
- "listing {} finished with state `{}`",
- view.operation, view.state
- )
- }),
- )),
- }
-}
-
-fn listing_transport_delivery_unavailable(view: &ListingMutationView) -> bool {
- matches!(
- view.source.as_str(),
- "Nostr transport publish · local key" | "SDK listing publish · configured signer"
- ) && (view.reason.as_deref().is_some_and(|reason| {
- reason.contains("configured Nostr relay")
- || reason.contains("Nostr transport connection failed")
- || reason.contains("SDK transport publish")
- }) || !view.target_transport_endpoints.is_empty()
- || !view.attempted_transport_endpoints.is_empty()
- || !view.failed_transport_targets.is_empty())
-}
-
-fn map_runtime<T>(
- operation_id: &str,
- result: Result<T, RuntimeError>,
-) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
-}
-
-fn required_string<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn required_path<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<PathBuf, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- optional_path(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn optional_path<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).map(PathBuf::from)
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request.payload.input().get(key).and_then(Value::as_bool)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value};
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::ListingOperationService;
- use crate::ops::{
- ListingCreateRequest, ListingListRequest, ListingPublishRequest, ListingWithdrawRequest,
- OperationAdapter, OperationContext, OperationData, OperationRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
-
- #[test]
- fn listing_service_requires_seller_actor_for_create_dry_run() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(ListingOperationService::new(&config));
- let context = OperationContext {
- dry_run: true,
- ..Default::default()
- };
- let request = OperationRequest::new(
- context.clone(),
- ListingCreateRequest::from_data(data(&[("key", "eggs"), ("title", "Eggs")])),
- )
- .expect("listing create request");
- let error = service
- .execute(request)
- .expect_err("listing create seller actor");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "account_unresolved");
- assert!(output_error.detail.expect("detail")["seller_actor_source"] == "resolved_account");
- }
-
- #[test]
- fn listing_service_exposes_listing_list_operation() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(ListingOperationService::new(&config));
- let request =
- OperationRequest::new(OperationContext::default(), ListingListRequest::default())
- .expect("listing list request");
- let envelope = service
- .execute(request)
- .expect("listing list result")
- .to_envelope(OperationContext::default().envelope_context("req_listing_list"))
- .expect("listing list envelope");
-
- assert_eq!(envelope.operation_id, "listing.list");
- assert_eq!(envelope.result["state"], "empty");
- assert_eq!(envelope.result["count"], 0);
- }
-
- #[test]
- fn listing_publish_and_withdraw_errors_do_not_use_retired_approval_language() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(ListingOperationService::new(&config));
- let retired_approval_language = ["approval", "token"].join("_");
- let publish = OperationRequest::new(
- OperationContext::default(),
- ListingPublishRequest::from_data(data(&[("file", "listing.toml")])),
- )
- .expect("listing publish request");
- let publish_error = service.execute(publish).expect_err("publish preflight");
- assert!(!format!("{publish_error}").contains(retired_approval_language.as_str()));
-
- let context = OperationContext {
- dry_run: true,
- ..Default::default()
- };
- let withdraw = OperationRequest::new(
- context.clone(),
- ListingWithdrawRequest::from_data(data(&[("file", "listing.toml")])),
- )
- .expect("listing withdraw request");
- let withdraw_error = service.execute(withdraw).expect_err("withdraw preflight");
- assert!(!format!("{withdraw_error}").contains(retired_approval_language.as_str()));
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-}
diff --git a/src/ops/exec/market.rs b/src/ops/exec/market.rs
@@ -1,708 +0,0 @@
-use serde::Serialize;
-use serde_json::Value;
-
-use crate::cli::global::{FindQueryArgs, RecordLookupArgs};
-use crate::ops::{
- MarketGetRequest, MarketGetResult, MarketPullRequest, MarketPullResult, MarketSearchRequest,
- MarketSearchResult, OperationAdapterError, OperationRequest, OperationRequestData,
- OperationRequestPayload, OperationResult, OperationResultData, OperationService,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{FindView, ListingGetView, SyncActionView};
-
-pub struct MarketOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> MarketOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<MarketPullRequest> for MarketOperationService<'_> {
- type Result = MarketPullResult;
-
- fn execute(
- &self,
- _request: OperationRequest<MarketPullRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = market_refresh_view(map_runtime(crate::runtime::sync::market_refresh(
- self.config,
- ))?);
- serialized_operation_result::<MarketPullResult, _>(&view)
- }
-}
-
-impl OperationService<MarketSearchRequest> for MarketOperationService<'_> {
- type Result = MarketSearchResult;
-
- fn execute(
- &self,
- request: OperationRequest<MarketSearchRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = FindQueryArgs {
- query: required_query_terms(&request)?,
- };
- let view = market_product_search_view(map_runtime(crate::runtime::find::search(
- self.config,
- &args,
- ))?);
- serialized_operation_result::<MarketSearchResult, _>(&view)
- }
-}
-
-impl OperationService<MarketGetRequest> for MarketOperationService<'_> {
- type Result = MarketGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<MarketGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = RecordLookupArgs {
- key: required_lookup(&request)?,
- };
- let view = market_listing_get_view(map_runtime(crate::runtime::listing::get(
- self.config,
- &args,
- ))?);
- serialized_operation_result::<MarketGetResult, _>(&view)
- }
-}
-
-fn market_refresh_view(mut view: SyncActionView) -> SyncActionView {
- view.actions = match view.state.as_str() {
- "ready" => vec!["radroots market search tomatoes".to_owned()],
- "unavailable" => vec!["radroots sync status".to_owned()],
- "unconfigured" => {
- let mut actions = Vec::new();
- if view.replica_store == "missing" {
- actions.push("radroots store inspect".to_owned());
- }
- if view.configured_transport_target_count == 0 {
- actions.push("radroots transport config update --kind nostr --nostr-relay wss://relay.example.com".to_owned());
- }
- if actions.is_empty() {
- actions.extend(std::mem::take(&mut view.actions));
- }
- actions
- }
- _ => std::mem::take(&mut view.actions),
- };
- view
-}
-
-fn market_product_search_view(mut view: FindView) -> FindView {
- view.actions = match view.state.as_str() {
- "ready" => view
- .results
- .first()
- .map(|result| {
- let mut actions = vec![format!("radroots market get {}", result.product_key)];
- if result.readiness.checkout_enabled {
- actions.push("radroots basket create".to_owned());
- }
- actions
- })
- .unwrap_or_default(),
- "empty" => vec![
- "radroots market pull".to_owned(),
- "radroots market search eggs".to_owned(),
- ],
- "unconfigured" => vec![
- "radroots store inspect".to_owned(),
- "radroots market pull".to_owned(),
- ],
- _ => std::mem::take(&mut view.actions),
- };
- view
-}
-
-fn market_listing_get_view(mut view: ListingGetView) -> ListingGetView {
- view.actions = match view.state.as_str() {
- "ready" => {
- if view.readiness.checkout_enabled {
- vec!["radroots basket create".to_owned()]
- } else {
- Vec::new()
- }
- }
- "missing" => vec![
- "radroots market search tomatoes".to_owned(),
- "radroots market pull".to_owned(),
- ],
- "unconfigured" => vec![
- "radroots store inspect".to_owned(),
- "radroots market pull".to_owned(),
- ],
- _ => std::mem::take(&mut view.actions),
- };
- view
-}
-
-fn required_query_terms<P>(
- request: &OperationRequest<P>,
-) -> Result<Vec<String>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- let input = request.payload.input();
- let Some(value) = input.get("query").or_else(|| input.get("terms")) else {
- return Err(invalid_input(
- request.operation_id(),
- "missing required `query` input".to_owned(),
- ));
- };
- let terms = match value {
- Value::String(value) => value
- .split_whitespace()
- .map(str::trim)
- .filter(|term| !term.is_empty())
- .map(str::to_owned)
- .collect::<Vec<_>>(),
- Value::Array(values) => values
- .iter()
- .map(|value| {
- value.as_str().map(str::to_owned).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- "`query` array entries must be strings".to_owned(),
- )
- })
- })
- .collect::<Result<Vec<_>, _>>()?,
- _ => {
- return Err(invalid_input(
- request.operation_id(),
- "`query` input must be a string or string array".to_owned(),
- ));
- }
- };
-
- if terms.is_empty() {
- return Err(invalid_input(
- request.operation_id(),
- "`query` input must not be empty".to_owned(),
- ));
- }
- Ok(terms)
-}
-
-fn required_lookup<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, "key")
- .or_else(|| string_input(request, "listing_id"))
- .or_else(|| string_input(request, "listing"))
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- "missing required `key` input".to_owned(),
- )
- })
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::Runtime(error.to_string()))
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use serde_json::{Map, Value};
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::{MarketOperationService, market_listing_get_view, market_product_search_view};
- use crate::ops::{
- MarketGetRequest, MarketPullRequest, MarketSearchRequest, OperationAdapter,
- OperationContext, OperationData, OperationRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
- use crate::view::runtime::{
- FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView,
- ListingGetView, MarketReadinessView, SyncFreshnessView,
- };
-
- const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-
- #[test]
- fn market_refresh_preserves_unconfigured_ingest_truth() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request =
- OperationRequest::new(OperationContext::default(), MarketPullRequest::default())
- .expect("market refresh request");
- let envelope = service
- .execute(request)
- .expect("market refresh result")
- .to_envelope(OperationContext::default().envelope_context("req_market_refresh"))
- .expect("market refresh envelope");
-
- assert_eq!(envelope.operation_id, "market.pull");
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["direction"], "pull");
- assert_eq!(
- envelope.result["actions"][0],
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
- );
- }
-
- #[test]
- fn market_refresh_supports_dry_run() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let context = OperationContext {
- dry_run: true,
- ..Default::default()
- };
- let request = OperationRequest::new(context.clone(), MarketPullRequest::default())
- .expect("market refresh request");
- let envelope = service
- .execute(request)
- .expect("market refresh dry run")
- .to_envelope(context.envelope_context("req_market_refresh"))
- .expect("market refresh envelope");
-
- assert_eq!(envelope.operation_id, "market.pull");
- assert!(envelope.dry_run);
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["replica_store"], "canonical");
- assert_eq!(envelope.result["direction"], "pull");
- }
-
- #[test]
- fn market_refresh_dry_run_skips_relay_fetch_when_store_is_ready() {
- let dir = tempdir().expect("tempdir");
- let mut config = sample_config(dir.path());
- config.output.dry_run = true;
- config.transport = crate::runtime::config::TransportConfig::from_nostr_relay_urls(vec![
- "wss://relay.example.com".to_owned(),
- ]);
- crate::runtime::store::init(&config).expect("store init");
-
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let context = OperationContext {
- dry_run: true,
- ..Default::default()
- };
- let request = OperationRequest::new(context.clone(), MarketPullRequest::default())
- .expect("market refresh request");
- let envelope = service
- .execute(request)
- .expect("market refresh dry run")
- .to_envelope(context.envelope_context("req_market_refresh"))
- .expect("market refresh envelope");
-
- assert_eq!(envelope.operation_id, "market.pull");
- assert_eq!(envelope.result["state"], "dry_run");
- assert_eq!(
- envelope.result["target_transport_endpoints"][0],
- "wss://relay.example.com"
- );
- assert_eq!(envelope.result["fetched_count"], 0);
- assert_eq!(envelope.result["ingested_count"], 0);
- assert_eq!(envelope.result["skipped_count"], 0);
- assert_eq!(envelope.result["unsupported_count"], 0);
- assert!(
- envelope.result["reason"]
- .as_str()
- .expect("reason")
- .contains("dry run")
- );
- }
-
- #[test]
- fn market_refresh_no_relay_action_is_actionable() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- crate::runtime::store::init(&config).expect("store init");
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request =
- OperationRequest::new(OperationContext::default(), MarketPullRequest::default())
- .expect("market refresh request");
- let envelope = service
- .execute(request)
- .expect("market refresh result")
- .to_envelope(OperationContext::default().envelope_context("req_market_refresh"))
- .expect("market refresh envelope");
-
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(
- envelope.result["actions"][0],
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
- );
- }
-
- #[test]
- fn market_product_search_uses_find_runtime_without_top_level_find() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request = OperationRequest::new(
- OperationContext::default(),
- MarketSearchRequest::from_data(data(&[("query", "eggs")])),
- )
- .expect("market product search request");
- let envelope = service
- .execute(request)
- .expect("market product search result")
- .to_envelope(OperationContext::default().envelope_context("req_market_search"))
- .expect("market product search envelope");
-
- assert_eq!(envelope.operation_id, "market.search");
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["query"], "eggs");
- assert_eq!(envelope.result["actions"][0], "radroots store inspect");
- }
-
- #[test]
- fn market_listing_get_requires_lookup_key() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request =
- OperationRequest::new(OperationContext::default(), MarketGetRequest::default())
- .expect("market listing get request");
- let error = service.execute(request).expect_err("key required");
-
- assert!(format!("{error}").contains("`key`"));
- }
-
- #[test]
- fn market_listing_get_wraps_listing_runtime_with_target_actions() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path());
- let service = OperationAdapter::new(MarketOperationService::new(&config));
- let request = OperationRequest::new(
- OperationContext::default(),
- MarketGetRequest::from_data(data(&[("key", "eggs")])),
- )
- .expect("market listing get request");
- let envelope = service
- .execute(request)
- .expect("market listing get result")
- .to_envelope(OperationContext::default().envelope_context("req_market_listing"))
- .expect("market listing get envelope");
-
- assert_eq!(envelope.operation_id, "market.get");
- assert_eq!(envelope.result["state"], "unconfigured");
- assert_eq!(envelope.result["actions"][0], "radroots store inspect");
- }
-
- #[test]
- fn market_ready_actions_do_not_emit_incomplete_basket_item_add_commands() {
- let search = market_product_search_view(FindView {
- state: "ready".to_owned(),
- source: "test".to_owned(),
- query: "eggs".to_owned(),
- count: 1,
- relay_count: 1,
- replica_store: "ready".to_owned(),
- freshness: freshness(),
- results: vec![FindResultView {
- id: "listing_eggs".to_owned(),
- product_key: "eggs".to_owned(),
- readiness: readiness_enabled(),
- listing_addr: Some(LISTING_ADDR.to_owned()),
- primary_bin_id: Some("bin-1".to_owned()),
- title: "Eggs".to_owned(),
- category: "eggs".to_owned(),
- summary: None,
- location_primary: None,
- available: quantity(),
- price: price(),
- provenance: provenance(),
- hyf: None,
- }],
- hyf: None,
- reason: None,
- actions: Vec::new(),
- });
-
- assert_eq!(
- search.actions,
- vec![
- "radroots market get eggs".to_owned(),
- "radroots basket create".to_owned()
- ]
- );
-
- let listing = market_listing_get_view(ListingGetView {
- state: "ready".to_owned(),
- source: "test".to_owned(),
- lookup: "eggs".to_owned(),
- readiness: readiness_enabled(),
- listing_id: Some("listing_eggs".to_owned()),
- product_key: Some("eggs".to_owned()),
- listing_addr: Some(LISTING_ADDR.to_owned()),
- primary_bin_id: Some("bin-1".to_owned()),
- title: Some("Eggs".to_owned()),
- category: Some("eggs".to_owned()),
- description: None,
- location_primary: None,
- available: Some(quantity()),
- price: Some(price()),
- provenance: provenance(),
- reason: None,
- actions: Vec::new(),
- });
-
- assert_eq!(listing.actions, vec!["radroots basket create".to_owned()]);
- assert!(
- search
- .actions
- .iter()
- .chain(listing.actions.iter())
- .all(|action| !action.starts_with("radroots basket item add "))
- );
- }
-
- #[test]
- fn market_ready_actions_require_checkout_enabled() {
- let disabled_search = market_product_search_view(FindView {
- state: "ready".to_owned(),
- source: "test".to_owned(),
- query: "eggs".to_owned(),
- count: 1,
- relay_count: 1,
- replica_store: "ready".to_owned(),
- freshness: freshness(),
- results: vec![FindResultView {
- id: "listing_eggs".to_owned(),
- product_key: "eggs".to_owned(),
- readiness: readiness_disabled(),
- listing_addr: Some(LISTING_ADDR.to_owned()),
- primary_bin_id: Some("bin-1".to_owned()),
- title: "Eggs".to_owned(),
- category: "eggs".to_owned(),
- summary: None,
- location_primary: None,
- available: quantity(),
- price: price(),
- provenance: provenance(),
- hyf: None,
- }],
- hyf: None,
- reason: None,
- actions: Vec::new(),
- });
-
- assert_eq!(
- disabled_search.actions,
- vec!["radroots market get eggs".to_owned()]
- );
-
- let disabled_listing = market_listing_get_view(ListingGetView {
- state: "ready".to_owned(),
- source: "test".to_owned(),
- lookup: "eggs".to_owned(),
- readiness: readiness_disabled(),
- listing_id: Some("listing_eggs".to_owned()),
- product_key: Some("eggs".to_owned()),
- listing_addr: Some(LISTING_ADDR.to_owned()),
- primary_bin_id: Some("bin-1".to_owned()),
- title: Some("Eggs".to_owned()),
- category: Some("eggs".to_owned()),
- description: None,
- location_primary: None,
- available: Some(quantity()),
- price: Some(price()),
- provenance: provenance(),
- reason: None,
- actions: Vec::new(),
- });
-
- assert!(disabled_listing.actions.is_empty());
- }
-
- fn freshness() -> SyncFreshnessView {
- SyncFreshnessView {
- state: "fresh".to_owned(),
- display: "fresh".to_owned(),
- age_seconds: Some(0),
- last_event_at: Some(0),
- run: None,
- }
- }
-
- fn provenance() -> FindResultProvenanceView {
- FindResultProvenanceView {
- origin: "fixture".to_owned(),
- freshness: "fresh".to_owned(),
- relay_count: 1,
- }
- }
-
- fn quantity() -> FindQuantityView {
- FindQuantityView {
- total_amount: 1.0,
- total_unit: "each".to_owned(),
- label: None,
- available_amount: Some(1),
- }
- }
-
- fn price() -> FindPriceView {
- FindPriceView {
- amount: 6.0,
- currency: "USD".to_owned(),
- per_amount: 1.0,
- per_unit: "each".to_owned(),
- }
- }
-
- fn readiness_enabled() -> MarketReadinessView {
- MarketReadinessView {
- protocol_valid: true,
- marketplace_eligible: true,
- checkout_enabled: true,
- primary_bin_verified: true,
- reason_codes: Vec::new(),
- }
- }
-
- fn readiness_disabled() -> MarketReadinessView {
- MarketReadinessView {
- protocol_valid: true,
- marketplace_eligible: true,
- checkout_enabled: false,
- primary_bin_verified: true,
- reason_codes: vec![
- "listing_checkout_disabled".to_owned(),
- "listing_inventory_unavailable".to_owned(),
- ],
- }
- }
-
- fn sample_config(root: &Path) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-
- fn data(entries: &[(&str, &str)]) -> OperationData {
- entries
- .iter()
- .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned())))
- .collect::<Map<String, Value>>()
- }
-}
diff --git a/src/ops/exec/mod.rs b/src/ops/exec/mod.rs
@@ -1,17 +0,0 @@
-pub mod basket;
-pub mod core;
-pub mod farm;
-pub mod listing;
-pub mod market;
-pub mod runtime;
-pub mod trade;
-pub mod validation;
-
-pub use basket::BasketOperationService;
-pub use core::CoreOperationService;
-pub use farm::FarmOperationService;
-pub use listing::ListingOperationService;
-pub use market::MarketOperationService;
-pub use runtime::RuntimeOperationService;
-pub use trade::TradeOperationService;
-pub use validation::ValidationOperationService;
diff --git a/src/ops/exec/runtime.rs b/src/ops/exec/runtime.rs
@@ -1,445 +0,0 @@
-use serde::Serialize;
-use serde_json::{Value, json};
-
-use crate::ops::{
- DiagnosticsInspectRequest, DiagnosticsInspectResult, OperationAdapterError, OperationRequest,
- OperationRequestData, OperationResult, OperationResultData, OperationService,
- SignerStatusRequest, SignerStatusResult, SyncPullRequest, SyncPullResult, SyncPushRequest,
- SyncPushResult, SyncStatusRequest, SyncStatusResult, TransportCapabilityListRequest,
- TransportCapabilityListResult, TransportConfigInspectRequest, TransportConfigInspectResult,
- TransportConfigUpdateRequest, TransportConfigUpdateResult, TransportDeliveryInspectRequest,
- TransportDeliveryInspectResult, TransportDeliveryRetryRequest, TransportDeliveryRetryResult,
- TransportStatusInspectRequest, TransportStatusInspectResult,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{CommandDisposition, SyncActionView, SyncStatusView};
-
-pub struct RuntimeOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> RuntimeOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<SignerStatusRequest> for RuntimeOperationService<'_> {
- type Result = SignerStatusResult;
-
- fn execute(
- &self,
- _request: OperationRequest<SignerStatusRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::signer::resolve_signer_status(self.config);
- serialized_operation_result::<SignerStatusResult, _>(&view)
- }
-}
-
-impl OperationService<TransportConfigInspectRequest> for RuntimeOperationService<'_> {
- type Result = TransportConfigInspectResult;
-
- fn execute(
- &self,
- _request: OperationRequest<TransportConfigInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::profile(self.config);
- serialized_operation_result::<TransportConfigInspectResult, _>(&view)
- }
-}
-
-impl OperationService<TransportConfigUpdateRequest> for RuntimeOperationService<'_> {
- type Result = TransportConfigUpdateResult;
-
- fn execute(
- &self,
- request: OperationRequest<TransportConfigUpdateRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime(
- request.operation_id(),
- crate::runtime::transport::set_profile(self.config, request.payload.input()),
- )?;
- serialized_operation_result::<TransportConfigUpdateResult, _>(&view)
- }
-}
-
-impl OperationService<TransportCapabilityListRequest> for RuntimeOperationService<'_> {
- type Result = TransportCapabilityListResult;
-
- fn execute(
- &self,
- _request: OperationRequest<TransportCapabilityListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let status = crate::runtime::transport::status(self.config);
- let view = json!({
- "state": "ready",
- "source": "Runtime Contract V1 transport capability registry",
- "active_profile": status.active_profile,
- "transports": status.transports,
- });
- serialized_operation_result::<TransportCapabilityListResult, _>(&view)
- }
-}
-
-impl OperationService<TransportStatusInspectRequest> for RuntimeOperationService<'_> {
- type Result = TransportStatusInspectResult;
-
- fn execute(
- &self,
- _request: OperationRequest<TransportStatusInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::status(self.config);
- serialized_operation_result::<TransportStatusInspectResult, _>(&view)
- }
-}
-
-impl OperationService<TransportDeliveryInspectRequest> for RuntimeOperationService<'_> {
- type Result = TransportDeliveryInspectResult;
-
- fn execute(
- &self,
- _request: OperationRequest<TransportDeliveryInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::outbox_status(self.config).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure("transport.delivery.inspect", error)
- })?;
- serialized_operation_result::<TransportDeliveryInspectResult, _>(&view)
- }
-}
-
-impl OperationService<TransportDeliveryRetryRequest> for RuntimeOperationService<'_> {
- type Result = TransportDeliveryRetryResult;
-
- fn execute(
- &self,
- request: OperationRequest<TransportDeliveryRetryRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::transport::outbox_push(self.config).map_err(|error| {
- OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
- })?;
- serialized_operation_result::<TransportDeliveryRetryResult, _>(&view)
- }
-}
-
-impl OperationService<SyncStatusRequest> for RuntimeOperationService<'_> {
- type Result = SyncStatusResult;
-
- fn execute(
- &self,
- _request: OperationRequest<SyncStatusRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::sync::status(self.config)
- .map_err(|error| OperationAdapterError::sdk_adapter_failure("sync.status", error))?;
- sync_status_result(&view)
- }
-}
-
-impl OperationService<SyncPullRequest> for RuntimeOperationService<'_> {
- type Result = SyncPullResult;
-
- fn execute(
- &self,
- _request: OperationRequest<SyncPullRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = map_runtime("sync.pull", crate::runtime::sync::pull(self.config))?;
- sync_action_result::<SyncPullResult>("sync.pull", &view)
- }
-}
-
-impl OperationService<SyncPushRequest> for RuntimeOperationService<'_> {
- type Result = SyncPushResult;
-
- fn execute(
- &self,
- _request: OperationRequest<SyncPushRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let view = crate::runtime::sync::push(self.config)
- .map_err(|error| OperationAdapterError::sdk_adapter_failure("sync.push", error))?;
- sync_action_result::<SyncPushResult>("sync.push", &view)
- }
-}
-
-impl OperationService<DiagnosticsInspectRequest> for RuntimeOperationService<'_> {
- type Result = DiagnosticsInspectResult;
-
- fn execute(
- &self,
- _request: OperationRequest<DiagnosticsInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let signer = crate::runtime::signer::resolve_signer_status(self.config);
- let transport = crate::runtime::transport::status(self.config);
- let sync = match crate::runtime::sync::status(self.config) {
- Ok(view) => serde_json::to_value(view).unwrap_or_else(|_| json!({ "state": "error" })),
- Err(error) => json!({
- "state": "unavailable",
- "reason": error.to_string(),
- }),
- };
- let state = if sync.get("state").and_then(Value::as_str) == Some("unavailable") {
- "degraded"
- } else {
- "ready"
- };
- let view = json!({
- "state": state,
- "source": "Runtime Contract V1 diagnostics",
- "signer": signer,
- "transport": transport,
- "sync": sync,
- });
- serialized_operation_result::<DiagnosticsInspectResult, _>(&view)
- }
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn sync_status_result(
- view: &SyncStatusView,
-) -> Result<OperationResult<SyncStatusResult>, OperationAdapterError> {
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<SyncStatusResult, _>(view),
- disposition => Err(sync_view_error(
- "sync.status",
- disposition,
- view,
- view.reason.as_deref(),
- )),
- }
-}
-
-fn sync_action_result<R>(
- operation_id: &str,
- view: &SyncActionView,
-) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
-{
- match view.disposition() {
- CommandDisposition::Success => serialized_operation_result::<R, _>(view),
- disposition => Err(sync_view_error(
- operation_id,
- disposition,
- view,
- view.reason.as_deref(),
- )),
- }
-}
-
-fn sync_view_error<T>(
- operation_id: &str,
- disposition: CommandDisposition,
- view: &T,
- reason: Option<&str>,
-) -> OperationAdapterError
-where
- T: Serialize,
-{
- let detail = serde_json::to_value(view).unwrap_or_else(|_| Value::Object(Default::default()));
- let message = reason
- .map(str::to_owned)
- .unwrap_or_else(|| format!("`{operation_id}` is not ready"));
- match disposition {
- CommandDisposition::Unconfigured => {
- OperationAdapterError::operation_unavailable_with_detail(operation_id, message, detail)
- }
- CommandDisposition::ExternalUnavailable => {
- OperationAdapterError::network_unavailable_with_detail(operation_id, message, detail)
- }
- CommandDisposition::Unsupported => OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::ValidationFailed => OperationAdapterError::ValidationFailed {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::NotFound => OperationAdapterError::NotFound {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::InternalError | CommandDisposition::Success => {
- OperationAdapterError::Runtime(message)
- }
- }
-}
-
-fn map_runtime<T>(
- operation_id: &str,
- result: Result<T, RuntimeError>,
-) -> Result<T, OperationAdapterError> {
- result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- use super::RuntimeOperationService;
- use crate::ops::{
- OperationAdapter, OperationContext, OperationRequest, SignerStatusRequest,
- SyncStatusRequest, TransportConfigInspectRequest,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
- LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat, PathsConfig, RpcConfig,
- RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
-
- #[test]
- fn runtime_service_backs_signer_and_transport_profile() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path(), vec!["wss://relay.test".into()]);
- let service = OperationAdapter::new(RuntimeOperationService::new(&config));
-
- let signer =
- OperationRequest::new(OperationContext::default(), SignerStatusRequest::default())
- .expect("signer status request");
- let signer_envelope = service
- .execute(signer)
- .expect("signer status result")
- .to_envelope(OperationContext::default().envelope_context("req_signer"))
- .expect("signer envelope");
- assert_eq!(signer_envelope.operation_id, "signer.status");
- assert_eq!(signer_envelope.result["state"], "unconfigured");
-
- let profile = OperationRequest::new(
- OperationContext::default(),
- TransportConfigInspectRequest::default(),
- )
- .expect("transport profile request");
- let profile_envelope = service
- .execute(profile)
- .expect("transport profile result")
- .to_envelope(OperationContext::default().envelope_context("req_transport"))
- .expect("transport profile envelope");
- assert_eq!(profile_envelope.operation_id, "transport.config.inspect");
- assert_eq!(profile_envelope.result["state"], "configured");
- assert_eq!(profile_envelope.result["profile_id"], "nostr");
- }
-
- #[test]
- fn runtime_service_backs_sync_status() {
- let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path(), Vec::new());
- let service = OperationAdapter::new(RuntimeOperationService::new(&config));
-
- let sync = OperationRequest::new(OperationContext::default(), SyncStatusRequest::default())
- .expect("sync status request");
- let envelope = service
- .execute(sync)
- .expect("sync status result")
- .to_envelope(OperationContext::default().envelope_context("req_sync_status"))
- .expect("sync status envelope");
-
- assert_eq!(envelope.operation_id, "sync.status");
- assert_eq!(envelope.result["state"], "degraded");
- assert_eq!(envelope.result["source"], "canonical SDK sync engine");
- assert_eq!(envelope.result["replica_store"], "canonical");
- assert_eq!(envelope.result["queue"]["pending_count"], 0);
- assert_eq!(envelope.result["queue"]["total_count"], 0);
- assert_eq!(
- envelope.result["actions"][0],
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
- );
- }
-
- fn sample_config(root: &Path, relays: Vec<String>) -> RuntimeConfig {
- let data = root.join("data");
- let cache = root.join("cache");
- let logs = root.join("logs");
- let secrets = root.join("secrets");
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "test".into(),
- allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
- root_source: "test".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: root.join("config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: data.join("apps/cli"),
- shared_cache_root: cache.clone(),
- app_logs_root: logs.join("apps/cli"),
- shared_accounts_data_root: data.join("shared/accounts"),
- shared_accounts_secrets_root: secrets.join("shared/accounts"),
- default_identity_path: secrets.join("shared/identities/default.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: false,
- },
- account: AccountConfig {
- selector: None,
- store_path: data.join("shared/accounts/store.json"),
- secrets_dir: secrets.join("shared/accounts"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: secrets.join("shared/identities/default.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::from_nostr_relay_urls(
- relays.clone(),
- ),
- local: LocalConfig {
- root: data.join("apps/cli/replica"),
- replica_store_path: data.join("apps/cli/replica/replica.sqlite"),
- backups_dir: data.join("apps/cli/replica/backups"),
- exports_dir: data.join("apps/cli/replica/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: false,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: Vec::new(),
- }
- }
-}
diff --git a/src/ops/exec/trade.rs b/src/ops/exec/trade.rs
@@ -1,377 +0,0 @@
-use std::path::PathBuf;
-
-use serde::Serialize;
-use serde_json::{Value, json};
-
-use crate::ops::{
- OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
- OperationResult, OperationResultData, OperationService, TradeCancellationSubmitRequest,
- TradeCancellationSubmitResult, TradeCandidateDecideRequest, TradeCandidateDecideResult,
- TradeEvidenceInspectRequest, TradeEvidenceInspectResult, TradeEvidenceRefreshRequest,
- TradeEvidenceRefreshResult, TradeGetRequest, TradeGetResult, TradeListRequest, TradeListResult,
- TradeOperationResumeRequest, TradeOperationResumeResult, TradePrivateArtifactDeleteRequest,
- TradePrivateArtifactDeleteResult, TradePrivateArtifactOpenRequest,
- TradePrivateArtifactOpenResult, TradePrivateArtifactSealRequest,
- TradePrivateArtifactSealResult, TradeProposalSubmitRequest, TradeProposalSubmitResult,
- TradeRevisionProposeRequest, TradeRevisionProposeResult,
-};
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::trade::{
- TradeEnvelopeFileRuntimeArgs, TradeEvidenceInspectRuntimeArgs, TradeIdRuntimeArgs,
- TradePageRuntimeArgs, TradePrivateArtifactDeleteRuntimeArgs,
- TradePrivateArtifactOpenRuntimeArgs, TradePrivateArtifactSealRuntimeArgs,
-};
-
-pub struct TradeOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> TradeOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<TradeProposalSubmitRequest> for TradeOperationService<'_> {
- type Result = TradeProposalSubmitResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeProposalSubmitRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = envelope_args(&request)?;
- let receipt = crate::runtime::trade::submit_proposal(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeRevisionProposeRequest> for TradeOperationService<'_> {
- type Result = TradeRevisionProposeResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeRevisionProposeRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = envelope_args(&request)?;
- let receipt = crate::runtime::trade::propose_revision(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeCandidateDecideRequest> for TradeOperationService<'_> {
- type Result = TradeCandidateDecideResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeCandidateDecideRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = envelope_args(&request)?;
- let receipt = crate::runtime::trade::decide_candidate(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeCancellationSubmitRequest> for TradeOperationService<'_> {
- type Result = TradeCancellationSubmitResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeCancellationSubmitRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = envelope_args(&request)?;
- let receipt = crate::runtime::trade::cancel_trade(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeOperationResumeRequest> for TradeOperationService<'_> {
- type Result = TradeOperationResumeResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeOperationResumeRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = envelope_args(&request)?;
- let receipt = crate::runtime::trade::resume_operation(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeGetRequest> for TradeOperationService<'_> {
- type Result = TradeGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeGetRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeIdRuntimeArgs {
- trade_id: required_string(&request, "trade_id")?,
- };
- let view = crate::runtime::trade::get_trade(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&view)
- }
-}
-
-impl OperationService<TradeListRequest> for TradeOperationService<'_> {
- type Result = TradeListResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeListRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradePageRuntimeArgs {
- limit: u32_input(&request, "limit")?,
- cursor: string_input(&request, "cursor"),
- };
- let page = crate::runtime::trade::list_trades(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&page)
- }
-}
-
-impl OperationService<TradeEvidenceRefreshRequest> for TradeOperationService<'_> {
- type Result = TradeEvidenceRefreshResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeEvidenceRefreshRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeIdRuntimeArgs {
- trade_id: required_string(&request, "trade_id")?,
- };
- let receipt = crate::runtime::trade::refresh_evidence(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradeEvidenceInspectRequest> for TradeOperationService<'_> {
- type Result = TradeEvidenceInspectResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradeEvidenceInspectRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradeEvidenceInspectRuntimeArgs {
- trade_id: required_string(&request, "trade_id")?,
- limit: u32_input(&request, "limit")?,
- cursor: string_input(&request, "cursor"),
- };
- let page = crate::runtime::trade::inspect_evidence(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&page)
- }
-}
-
-impl OperationService<TradePrivateArtifactSealRequest> for TradeOperationService<'_> {
- type Result = TradePrivateArtifactSealResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradePrivateArtifactSealRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradePrivateArtifactSealRuntimeArgs {
- trade_id: required_string(&request, "trade_id")?,
- artifact_id: required_string(&request, "artifact_id")?,
- schema_id: required_string(&request, "schema_id")?,
- input: required_path(&request, "input")?,
- kind: string_input(&request, "kind").unwrap_or_else(|| "binding_terms".to_owned()),
- candidate_id: string_input(&request, "candidate_id"),
- retention_class: string_input(&request, "retention_class"),
- expires_at_ms: i64_input(&request, "expires_at_ms")?,
- };
- let receipt = crate::runtime::trade::seal_private_artifact(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-impl OperationService<TradePrivateArtifactOpenRequest> for TradeOperationService<'_> {
- type Result = TradePrivateArtifactOpenResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradePrivateArtifactOpenRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradePrivateArtifactOpenRuntimeArgs {
- artifact_id: required_string(&request, "artifact_id")?,
- output: required_path(&request, "output")?,
- };
- let view = crate::runtime::trade::open_private_artifact(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- if view.state == "missing" {
- return Err(OperationAdapterError::not_found_with_detail(
- request.operation_id(),
- format!("private artifact `{}` was not found", view.artifact_id),
- json!({
- "artifact_id": view.artifact_id,
- "output": view.output,
- }),
- ));
- }
- serialized_target_result(&view)
- }
-}
-
-impl OperationService<TradePrivateArtifactDeleteRequest> for TradeOperationService<'_> {
- type Result = TradePrivateArtifactDeleteResult;
-
- fn execute(
- &self,
- request: OperationRequest<TradePrivateArtifactDeleteRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let args = TradePrivateArtifactDeleteRuntimeArgs {
- artifact_id: required_string(&request, "artifact_id")?,
- };
- let receipt = crate::runtime::trade::delete_private_artifact(self.config, &args)
- .map_err(|error| sdk_failure(request.operation_id(), error))?;
- serialized_target_result(&receipt)
- }
-}
-
-fn envelope_args<P>(
- request: &OperationRequest<P>,
-) -> Result<TradeEnvelopeFileRuntimeArgs, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- Ok(TradeEnvelopeFileRuntimeArgs {
- file: required_path(request, "file")?,
- idempotency_key: request
- .context
- .idempotency_key
- .clone()
- .or_else(|| string_input(request, "idempotency_key")),
- acknowledge_private_terms: bool_input(request, "acknowledge_private_terms")
- .unwrap_or(false),
- operation_kind: string_input(request, "operation_kind"),
- })
-}
-
-fn serialized_target_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
-
-fn required_string<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<String, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key).ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .and_then(Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned)
-}
-
-fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request.payload.input().get(key).and_then(Value::as_bool)
-}
-
-fn u32_input<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<Option<u32>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .map(|value| {
- value
- .as_u64()
- .and_then(|value| u32::try_from(value).ok())
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("`{key}` must be an unsigned 32-bit integer"),
- )
- })
- })
- .transpose()
-}
-
-fn i64_input<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<Option<i64>, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- request
- .payload
- .input()
- .get(key)
- .map(|value| {
- value.as_i64().ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("`{key}` must be a signed 64-bit integer"),
- )
- })
- })
- .transpose()
-}
-
-fn required_path<P>(
- request: &OperationRequest<P>,
- key: &str,
-) -> Result<PathBuf, OperationAdapterError>
-where
- P: OperationRequestPayload + OperationRequestData,
-{
- string_input(request, key)
- .map(PathBuf::from)
- .ok_or_else(|| {
- invalid_input(
- request.operation_id(),
- format!("missing required `{key}` input"),
- )
- })
-}
-
-fn sdk_failure(
- operation_id: &str,
- error: crate::runtime::sdk::CliSdkAdapterError,
-) -> OperationAdapterError {
- OperationAdapterError::sdk_adapter_failure(operation_id, error)
-}
-
-fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError {
- OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- }
-}
diff --git a/src/ops/exec/validation.rs b/src/ops/exec/validation.rs
@@ -1,48 +0,0 @@
-use serde::Serialize;
-use serde_json::json;
-
-use crate::ops::{
- OperationAdapterError, OperationRequest, OperationResult, OperationResultData,
- OperationService, ValidationStatusRequest, ValidationStatusResult,
-};
-use crate::runtime::config::RuntimeConfig;
-
-pub struct ValidationOperationService<'a> {
- config: &'a RuntimeConfig,
-}
-
-impl<'a> ValidationOperationService<'a> {
- pub fn new(config: &'a RuntimeConfig) -> Self {
- Self { config }
- }
-}
-
-impl OperationService<ValidationStatusRequest> for ValidationOperationService<'_> {
- type Result = ValidationStatusResult;
-
- fn execute(
- &self,
- _request: OperationRequest<ValidationStatusRequest>,
- ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- let state = if self.config.rhi.validator_set.is_some() {
- "ready"
- } else {
- "unconfigured"
- };
- let view = json!({
- "state": state,
- "source": "Runtime Contract V1 validation configuration",
- "validator_set_configured": self.config.rhi.validator_set.is_some(),
- "cryptographic_proof_required": self.config.rhi.require_cryptographic_proof,
- });
- serialized_operation_result::<ValidationStatusResult, _>(&view)
- }
-}
-
-fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
-where
- R: OperationResultData,
- T: Serialize,
-{
- OperationResult::new(R::from_serializable(value)?)
-}
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
@@ -1,14 +0,0 @@
-mod adapter;
-mod context;
-mod error;
-pub mod exec;
-mod request;
-mod result;
-mod target;
-
-pub use adapter::*;
-pub use context::*;
-pub use error::OperationAdapterError;
-pub use request::*;
-pub use result::*;
-pub use target::*;
diff --git a/src/ops/request.rs b/src/ops/request.rs
@@ -1,52 +0,0 @@
-use std::fmt::Debug;
-
-use serde_json::{Map, Value};
-
-use super::context::OperationContext;
-use super::error::OperationAdapterError;
-use crate::registry::{OperationSpec, get_operation};
-
-pub type OperationData = Map<String, Value>;
-
-pub trait OperationRequestPayload: Debug + Clone + PartialEq + 'static {
- const OPERATION_ID: &'static str;
- const REQUEST_TYPE: &'static str;
-}
-
-pub trait OperationRequestData: OperationRequestPayload {
- fn input(&self) -> &OperationData;
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct OperationRequest<P: OperationRequestPayload> {
- pub spec: &'static OperationSpec,
- pub context: OperationContext,
- pub payload: P,
-}
-
-impl<P: OperationRequestPayload> OperationRequest<P> {
- pub fn new(context: OperationContext, payload: P) -> Result<Self, OperationAdapterError> {
- let spec = get_operation(P::OPERATION_ID)
- .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?;
- if spec.rust_request != P::REQUEST_TYPE {
- return Err(OperationAdapterError::RequestTypeMismatch {
- operation_id: P::OPERATION_ID.to_owned(),
- registry_request: spec.rust_request.to_owned(),
- adapter_request: P::REQUEST_TYPE.to_owned(),
- });
- }
- Ok(Self {
- spec,
- context,
- payload,
- })
- }
-
- pub fn operation_id(&self) -> &'static str {
- P::OPERATION_ID
- }
-
- pub fn request_type_name(&self) -> &'static str {
- P::REQUEST_TYPE
- }
-}
diff --git a/src/ops/result.rs b/src/ops/result.rs
@@ -1,98 +0,0 @@
-use std::fmt::Debug;
-
-use serde::Serialize;
-use serde_json::Value;
-
-use super::error::OperationAdapterError;
-use super::request::OperationData;
-use crate::out::envelope::{
- EnvelopeContext, NextAction, OutputEnvelope, OutputWarning, next_actions_from_result_value,
-};
-use crate::registry::{OperationSpec, get_operation};
-
-pub trait OperationResultPayload: Debug + Clone + PartialEq + Serialize + 'static {
- const OPERATION_ID: &'static str;
- const RESULT_TYPE: &'static str;
-}
-
-pub trait OperationResultData: OperationResultPayload + Sized {
- fn from_data(data: OperationData) -> Self;
-
- fn from_value(value: Value) -> Self {
- Self::from_data(value_to_data(value))
- }
-
- fn from_serializable<T: Serialize>(value: &T) -> Result<Self, OperationAdapterError> {
- Ok(Self::from_value(serde_json::to_value(value).map_err(
- |error| OperationAdapterError::Serialization(error.to_string()),
- )?))
- }
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct OperationResult<P: OperationResultPayload> {
- pub spec: &'static OperationSpec,
- pub payload: P,
- pub warnings: Vec<OutputWarning>,
- pub next_actions: Vec<NextAction>,
-}
-
-impl<P: OperationResultPayload> OperationResult<P> {
- pub fn new(payload: P) -> Result<Self, OperationAdapterError> {
- let spec = get_operation(P::OPERATION_ID)
- .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?;
- if spec.rust_result != P::RESULT_TYPE {
- return Err(OperationAdapterError::ResultTypeMismatch {
- operation_id: P::OPERATION_ID.to_owned(),
- registry_result: spec.rust_result.to_owned(),
- adapter_result: P::RESULT_TYPE.to_owned(),
- });
- }
- Ok(Self {
- spec,
- payload,
- warnings: Vec::new(),
- next_actions: Vec::new(),
- })
- }
-
- pub fn operation_id(&self) -> &'static str {
- P::OPERATION_ID
- }
-
- pub fn result_type_name(&self) -> &'static str {
- P::RESULT_TYPE
- }
-
- pub fn to_envelope(
- &self,
- context: EnvelopeContext,
- ) -> Result<OutputEnvelope, OperationAdapterError> {
- let result = serde_json::to_value(&self.payload)
- .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?;
- let next_actions = if self.next_actions.is_empty() {
- next_actions_from_result(&result)
- } else {
- self.next_actions.clone()
- };
- let mut envelope = OutputEnvelope::success(self.operation_id(), result, context);
- envelope.warnings = self.warnings.clone();
- envelope.next_actions = next_actions;
- Ok(envelope)
- }
-}
-
-fn next_actions_from_result(result: &Value) -> Vec<NextAction> {
- next_actions_from_result_value(result)
-}
-
-pub(crate) fn value_to_data(value: Value) -> OperationData {
- match value {
- Value::Object(map) => map,
- other => {
- let mut map = OperationData::new();
- map.insert("value".to_owned(), other);
- map
- }
- }
-}
diff --git a/src/ops/target.rs b/src/ops/target.rs
@@ -1,186 +0,0 @@
-use serde::Serialize;
-use serde_json::Value;
-
-use super::context::{OperationContext, OperationOutputFormat};
-use super::error::OperationAdapterError;
-use super::request::{
- OperationData, OperationRequest, OperationRequestData, OperationRequestPayload,
-};
-use super::result::{OperationResult, OperationResultData, OperationResultPayload, value_to_data};
-use crate::cli::TargetCliArgs;
-use crate::registry::{OPERATION_REGISTRY, OperationSpec};
-
-macro_rules! target_operation_contracts {
- ($( $variant:ident => ($request:ident, $result:ident, $operation_id:literal) ),+ $(,)?) => {
- #[derive(Debug, Clone, PartialEq)]
- pub enum TargetOperationRequest {
- $( $variant(OperationRequest<$request>), )+
- }
-
- impl TargetOperationRequest {
- pub fn from_target_args(args: &TargetCliArgs) -> Result<Self, OperationAdapterError> {
- let operation_id = crate::cli::input::operation_id_from_target(args);
- Self::from_operation_id_with_input(
- operation_id,
- OperationContext::from_target_args(args, operation_id)?,
- crate::cli::input::target_operation_input(&args.command),
- )
- }
-
- pub fn from_operation_id(
- operation_id: &'static str,
- context: OperationContext,
- ) -> Result<Self, OperationAdapterError> {
- Self::from_operation_id_with_input(operation_id, context, OperationData::new())
- }
-
- fn from_operation_id_with_input(
- operation_id: &'static str,
- context: OperationContext,
- input: OperationData,
- ) -> Result<Self, OperationAdapterError> {
- match operation_id {
- $( $operation_id => Ok(Self::$variant(OperationRequest::new(context, $request::from_data(input))?)), )+
- _ => Err(OperationAdapterError::UnknownOperation(operation_id.to_owned())),
- }
- }
-
- pub fn operation_id(&self) -> &'static str {
- match self {
- $( Self::$variant(request) => request.operation_id(), )+
- }
- }
-
- pub fn spec(&self) -> &'static OperationSpec {
- match self {
- $( Self::$variant(request) => request.spec, )+
- }
- }
-
- pub fn context(&self) -> &OperationContext {
- match self {
- $( Self::$variant(request) => &request.context, )+
- }
- }
-
- pub fn set_output_format(&mut self, output_format: OperationOutputFormat) {
- match self {
- $( Self::$variant(request) => request.context.output_format = output_format, )+
- }
- }
-
- pub fn request_type_name(&self) -> &'static str {
- match self {
- $( Self::$variant(request) => request.request_type_name(), )+
- }
- }
-
- pub fn request_type_for_operation(operation_id: &str) -> Option<&'static str> {
- match operation_id {
- $( $operation_id => Some(stringify!($request)), )+
- _ => None,
- }
- }
- }
-
- #[derive(Debug, Clone, PartialEq)]
- pub enum TargetOperationResult {
- $( $variant(OperationResult<$result>), )+
- }
-
- impl TargetOperationResult {
- pub fn operation_id(&self) -> &'static str {
- match self {
- $( Self::$variant(result) => result.operation_id(), )+
- }
- }
-
- pub fn result_type_name(&self) -> &'static str {
- match self {
- $( Self::$variant(result) => result.result_type_name(), )+
- }
- }
-
- pub fn result_type_for_operation(operation_id: &str) -> Option<&'static str> {
- match operation_id {
- $( $operation_id => Some(stringify!($result)), )+
- _ => None,
- }
- }
- }
-
- $(
- #[derive(Debug, Default, Clone, PartialEq, Serialize)]
- pub struct $request {
- #[serde(flatten)]
- pub input: OperationData,
- }
-
- impl $request {
- pub fn from_data(input: OperationData) -> Self {
- Self { input }
- }
- }
-
- impl OperationRequestPayload for $request {
- const OPERATION_ID: &'static str = $operation_id;
- const REQUEST_TYPE: &'static str = stringify!($request);
- }
-
- impl OperationRequestData for $request {
- fn input(&self) -> &OperationData {
- &self.input
- }
- }
-
- #[derive(Debug, Default, Clone, PartialEq, Serialize)]
- pub struct $result {
- #[serde(flatten)]
- pub data: OperationData,
- }
-
- impl $result {
- pub fn from_data(data: OperationData) -> Self {
- Self { data }
- }
-
- pub fn from_value(value: Value) -> Self {
- Self {
- data: value_to_data(value),
- }
- }
-
- pub fn from_serializable<T: Serialize>(
- value: &T,
- ) -> Result<Self, OperationAdapterError> {
- Ok(Self::from_value(
- serde_json::to_value(value)
- .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?,
- ))
- }
- }
-
- impl OperationResultPayload for $result {
- const OPERATION_ID: &'static str = $operation_id;
- const RESULT_TYPE: &'static str = stringify!($result);
- }
-
- impl OperationResultData for $result {
- fn from_data(data: OperationData) -> Self {
- Self { data }
- }
- }
- )+
- };
-}
-
-include!("../generated/runtime_contract_target.rs");
-
-pub fn adapter_registry_linkage_is_valid() -> bool {
- OPERATION_REGISTRY.iter().all(|operation| {
- TargetOperationRequest::request_type_for_operation(operation.operation_id)
- == Some(operation.rust_request)
- && TargetOperationResult::result_type_for_operation(operation.operation_id)
- == Some(operation.rust_result)
- })
-}
diff --git a/src/out/envelope.rs b/src/out/envelope.rs
@@ -1,726 +0,0 @@
-use serde::Serialize;
-use serde_json::{Value, json};
-
-pub const OUTPUT_SCHEMA_VERSION: &str = "radroots.cli.output.v1";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct EnvelopeContext {
- pub request_id: String,
- pub correlation_id: Option<String>,
- pub idempotency_key: Option<String>,
- pub output_format: OutputFormat,
- pub dry_run: bool,
- pub actor: Option<EnvelopeActor>,
-}
-
-impl EnvelopeContext {
- pub fn new(request_id: impl Into<String>, dry_run: bool) -> Self {
- Self {
- request_id: request_id.into(),
- correlation_id: None,
- idempotency_key: None,
- output_format: OutputFormat::Terminal,
- dry_run,
- actor: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct EnvelopeActor {
- pub account_id: String,
- pub role: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Serialize)]
-pub struct OutputEnvelope {
- pub schema_version: &'static str,
- pub operation_id: String,
- pub kind: String,
- pub status: OutputStatus,
- pub output_format: OutputFormat,
- pub request_id: String,
- pub correlation_id: Option<String>,
- pub idempotency_key: Option<String>,
- pub dry_run: bool,
- pub actor: Option<EnvelopeActor>,
- pub resource: Option<OutputResource>,
- pub result: Value,
- pub reason_code: Option<String>,
- pub warnings: Vec<OutputWarning>,
- pub errors: Vec<OutputError>,
- pub next_actions: Vec<NextAction>,
-}
-
-impl OutputEnvelope {
- pub fn success(
- operation_id: impl Into<String>,
- result: Value,
- context: EnvelopeContext,
- ) -> Self {
- let operation_id = operation_id.into();
- let resource = output_resource_from_value(&result);
- let reason_code = output_reason_code_from_value(&result);
- Self {
- schema_version: OUTPUT_SCHEMA_VERSION,
- kind: operation_id.clone(),
- operation_id,
- status: OutputStatus::Ok,
- output_format: context.output_format,
- request_id: context.request_id,
- correlation_id: context.correlation_id,
- idempotency_key: context.idempotency_key,
- dry_run: context.dry_run,
- actor: context.actor,
- resource,
- result,
- reason_code,
- warnings: Vec::new(),
- errors: Vec::new(),
- next_actions: Vec::new(),
- }
- }
-
- pub fn failure(
- operation_id: impl Into<String>,
- error: OutputError,
- context: EnvelopeContext,
- ) -> Self {
- let operation_id = operation_id.into();
- let next_actions = next_actions_from_error_detail(&error);
- let resource = error.detail.as_ref().and_then(output_resource_from_value);
- let reason_code = Some(error.reason_code.clone());
- Self {
- schema_version: OUTPUT_SCHEMA_VERSION,
- kind: operation_id.clone(),
- operation_id,
- status: OutputStatus::Error,
- output_format: context.output_format,
- request_id: context.request_id,
- correlation_id: context.correlation_id,
- idempotency_key: context.idempotency_key,
- dry_run: context.dry_run,
- actor: context.actor,
- resource,
- result: Value::Null,
- reason_code,
- warnings: Vec::new(),
- errors: vec![error],
- next_actions,
- }
- }
-
- pub fn to_ndjson_frames(&self) -> Vec<NdjsonFrame> {
- let started = NdjsonFrame::new(
- self.operation_id.clone(),
- self.request_id.clone(),
- 0,
- NdjsonFrameType::Started,
- json!({
- "state": "started",
- "status": self.status,
- "output_format": self.output_format,
- "dry_run": self.dry_run,
- "correlation_id": &self.correlation_id,
- "idempotency_key": &self.idempotency_key,
- "actor": &self.actor,
- "resource": &self.resource,
- }),
- );
- let mut terminal = NdjsonFrame::new(
- self.operation_id.clone(),
- self.request_id.clone(),
- 1,
- if self.errors.is_empty() {
- NdjsonFrameType::Completed
- } else {
- NdjsonFrameType::Error
- },
- json!({
- "status": self.status,
- "reason_code": &self.reason_code,
- "output_format": self.output_format,
- "resource": &self.resource,
- "result": &self.result,
- "next_actions": &self.next_actions,
- "dry_run": self.dry_run,
- "correlation_id": &self.correlation_id,
- "idempotency_key": &self.idempotency_key,
- "actor": &self.actor,
- }),
- );
- terminal.warnings = self.warnings.clone();
- terminal.errors = self.errors.clone();
- vec![started, terminal]
- }
-}
-
-fn output_reason_code_from_value(value: &Value) -> Option<String> {
- value
- .get("reason_code")
- .and_then(Value::as_str)
- .filter(|reason_code| !reason_code.trim().is_empty())
- .map(str::to_owned)
-}
-
-fn output_resource_from_value(value: &Value) -> Option<OutputResource> {
- let object = value.as_object()?;
- if let Some(resource) = object.get("resource").and_then(declared_output_resource) {
- return Some(resource);
- }
- output_resource_from_fields(object).or_else(|| {
- let nested_fields = [
- "account",
- "resolved_account",
- "default_account",
- "bound_account",
- "farm",
- "listing",
- "basket",
- "quote",
- "trade",
- ];
- nested_fields
- .into_iter()
- .filter_map(|field| {
- object
- .get(field)
- .and_then(|value| nested_output_resource(field, value))
- })
- .next()
- })
-}
-
-fn nested_output_resource(field: &str, value: &Value) -> Option<OutputResource> {
- let mut resource = output_resource_from_value(value)?;
- if resource.kind == "resource" {
- resource.kind = match field {
- "resolved_account" | "default_account" | "bound_account" => "account",
- "order" => "trade",
- other => other,
- }
- .to_owned();
- }
- Some(resource)
-}
-
-fn declared_output_resource(value: &Value) -> Option<OutputResource> {
- let object = value.as_object()?;
- let kind = object
- .get("kind")
- .and_then(Value::as_str)
- .filter(|kind| !kind.trim().is_empty())?;
- let id = object
- .get("id")
- .and_then(Value::as_str)
- .filter(|id| !id.trim().is_empty())?;
- Some(OutputResource {
- kind: kind.to_owned(),
- id: id.to_owned(),
- })
-}
-
-fn output_resource_from_fields(object: &serde_json::Map<String, Value>) -> Option<OutputResource> {
- [
- ("account_id", "account"),
- ("id", "resource"),
- ("farm_id", "farm"),
- ("seller_account_id", "account"),
- ("buyer_account_id", "account"),
- ("listing_id", "listing"),
- ("listing_address", "listing"),
- ("listing_addr", "listing"),
- ("basket_id", "basket"),
- ("order_id", "trade"),
- ]
- .into_iter()
- .find_map(|(field, kind)| {
- object
- .get(field)
- .and_then(Value::as_str)
- .filter(|id| !id.trim().is_empty())
- .map(|id| OutputResource {
- kind: kind.to_owned(),
- id: id.to_owned(),
- })
- })
-}
-
-pub fn next_actions_from_result_value(result: &Value) -> Vec<NextAction> {
- next_actions_from_actions_value(result.get("actions"))
-}
-
-fn next_actions_from_error_detail(error: &OutputError) -> Vec<NextAction> {
- next_actions_from_actions_value(
- error
- .detail
- .as_ref()
- .and_then(|detail| detail.get("actions")),
- )
-}
-
-fn next_actions_from_actions_value(actions_value: Option<&Value>) -> Vec<NextAction> {
- actions_value
- .and_then(Value::as_array)
- .into_iter()
- .flatten()
- .filter_map(Value::as_str)
- .filter_map(next_action_from_action_string)
- .fold(Vec::<NextAction>::new(), |mut actions, action| {
- if !actions.contains(&action) {
- actions.push(action);
- }
- actions
- })
-}
-
-fn next_action_from_action_string(action: &str) -> Option<NextAction> {
- let action = action.trim();
- if action == "configure signer.remote_nip46 signer_session_ref" {
- return Some(NextAction {
- kind: NextActionKind::OperatorConfig,
- label: "configure signer session binding".to_owned(),
- command: None,
- description: Some(action.to_owned()),
- env_var: None,
- config_key: Some("signer.remote_nip46.signer_session_ref".to_owned()),
- });
- }
- if action
- == "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID"
- {
- return Some(NextAction {
- kind: NextActionKind::OperatorConfig,
- label: "configure radrootsd execution token source".to_owned(),
- command: None,
- description: Some(action.to_owned()),
- env_var: Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()),
- config_key: None,
- });
- }
- let command = action.trim().strip_prefix("run ").unwrap_or(action).trim();
- if !command.starts_with("radroots ") {
- return None;
- }
- Some(NextAction {
- kind: NextActionKind::CliCommand,
- label: next_action_label(command),
- command: Some(command.to_owned()),
- description: None,
- env_var: None,
- config_key: None,
- })
-}
-
-fn next_action_label(command: &str) -> String {
- let parts = command.split_whitespace().collect::<Vec<_>>();
- let mut index = usize::from(parts.first().is_some_and(|part| *part == "radroots"));
- let mut labels = Vec::new();
- while index < parts.len() {
- let part = parts[index];
- if part.starts_with("--") {
- index += 1;
- if matches!(
- part,
- "--format"
- | "--account-id"
- | "--idempotency-key"
- | "--correlation-id"
- | "--approval-proof"
- ) && index < parts.len()
- {
- index += 1;
- }
- continue;
- }
- labels.push(part);
- index += 1;
- }
- if labels.is_empty() {
- "radroots".to_owned()
- } else {
- labels.join(" ")
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct OutputWarning {
- pub code: String,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum OutputStatus {
- Ok,
- Error,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum OutputFormat {
- Terminal,
- Json,
- Ndjson,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct OutputResource {
- pub kind: String,
- pub id: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Serialize)]
-pub struct OutputError {
- pub code: String,
- pub reason_code: String,
- pub message: String,
- pub exit_code: u8,
- pub detail: Option<Value>,
-}
-
-impl OutputError {
- pub fn new(
- code: impl Into<String>,
- message: impl Into<String>,
- exit_code: CliExitCode,
- ) -> Self {
- let code = code.into();
- Self {
- reason_code: code.clone(),
- code,
- message: message.into(),
- exit_code: exit_code.code(),
- detail: None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CliExitCode {
- Success,
- InternalError,
- InvalidInput,
- RuntimeUnavailable,
- NotFound,
- AuthorizationFailed,
- ApprovalRequiredOrDenied,
- SignerUnavailable,
- SyncOrNetworkFailure,
- Conflict,
- ValidationFailed,
- UnsafeOperationRefused,
-}
-
-impl CliExitCode {
- pub fn code(self) -> u8 {
- match self {
- Self::Success => 0,
- Self::InternalError => 1,
- Self::InvalidInput => 2,
- Self::RuntimeUnavailable => 3,
- Self::NotFound => 4,
- Self::AuthorizationFailed => 5,
- Self::ApprovalRequiredOrDenied => 6,
- Self::SignerUnavailable => 7,
- Self::SyncOrNetworkFailure => 8,
- Self::Conflict => 9,
- Self::ValidationFailed => 10,
- Self::UnsafeOperationRefused => 11,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-pub struct NextAction {
- pub kind: NextActionKind,
- pub label: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub command: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub description: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub env_var: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config_key: Option<String>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum NextActionKind {
- CliCommand,
- OperatorConfig,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum NdjsonFrameType {
- Started,
- Event,
- Progress,
- Warning,
- Error,
- Completed,
- Heartbeat,
-}
-
-#[derive(Debug, Clone, PartialEq, Serialize)]
-pub struct NdjsonFrame {
- pub schema_version: &'static str,
- pub operation_id: String,
- pub kind: String,
- pub request_id: String,
- pub sequence: u64,
- pub frame_type: NdjsonFrameType,
- pub payload: Value,
- pub warnings: Vec<OutputWarning>,
- pub errors: Vec<OutputError>,
-}
-
-impl NdjsonFrame {
- pub fn new(
- operation_id: impl Into<String>,
- request_id: impl Into<String>,
- sequence: u64,
- frame_type: NdjsonFrameType,
- payload: Value,
- ) -> Self {
- let operation_id = operation_id.into();
- Self {
- schema_version: OUTPUT_SCHEMA_VERSION,
- kind: operation_id.clone(),
- operation_id,
- request_id: request_id.into(),
- sequence,
- frame_type,
- payload,
- warnings: Vec::new(),
- errors: Vec::new(),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::{Value, json};
-
- use super::{
- CliExitCode, EnvelopeContext, NdjsonFrame, NdjsonFrameType, NextActionKind,
- OUTPUT_SCHEMA_VERSION, OutputEnvelope, OutputError,
- };
-
- #[test]
- fn success_envelope_serializes_required_fields() {
- let mut context = EnvelopeContext::new("req_test", true);
- context.correlation_id = Some("corr_test".to_owned());
- context.idempotency_key = Some("idem_test".to_owned());
- let envelope = OutputEnvelope::success(
- "listing.publish",
- json!({ "listing_id": "listing_test" }),
- context,
- );
- let value = serde_json::to_value(envelope).expect("serialize envelope");
-
- assert_eq!(value["schema_version"], OUTPUT_SCHEMA_VERSION);
- assert_eq!(value["operation_id"], "listing.publish");
- assert_eq!(value["kind"], "listing.publish");
- assert_eq!(value["status"], "ok");
- assert_eq!(value["output_format"], "terminal");
- assert_eq!(value["request_id"], "req_test");
- assert_eq!(value["correlation_id"], "corr_test");
- assert_eq!(value["idempotency_key"], "idem_test");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["resource"]["kind"], "listing");
- assert_eq!(value["resource"]["id"], "listing_test");
- assert_eq!(value["result"]["listing_id"], "listing_test");
- assert_eq!(value["reason_code"], Value::Null);
- assert_eq!(value["warnings"].as_array().unwrap().len(), 0);
- assert_eq!(value["errors"].as_array().unwrap().len(), 0);
- assert_eq!(value["next_actions"].as_array().unwrap().len(), 0);
- }
-
- #[test]
- fn failure_envelope_carries_structured_error_and_exit_code() {
- let error = OutputError::new(
- "approval_required",
- "operation requires approval proof",
- CliExitCode::ApprovalRequiredOrDenied,
- );
- let envelope = OutputEnvelope::failure(
- "trade.proposal.submit",
- error,
- EnvelopeContext::new("req_order", false),
- );
- let value = serde_json::to_value(envelope).expect("serialize envelope");
-
- assert_eq!(value["schema_version"], OUTPUT_SCHEMA_VERSION);
- assert_eq!(value["operation_id"], "trade.proposal.submit");
- assert_eq!(value["status"], "error");
- assert_eq!(value["reason_code"], "approval_required");
- assert_eq!(value["result"], Value::Null);
- assert_eq!(value["errors"][0]["code"], "approval_required");
- assert_eq!(value["errors"][0]["reason_code"], "approval_required");
- assert_eq!(value["errors"][0]["exit_code"], 6);
- }
-
- #[test]
- fn failure_envelope_derives_next_actions_from_error_detail() {
- let mut error = OutputError::new(
- "not_found",
- "order draft was not found",
- CliExitCode::NotFound,
- );
- error.detail = Some(json!({
- "actions": [
- "radroots trade list",
- "run radroots basket create"
- ]
- }));
- let envelope = OutputEnvelope::failure(
- "trade.proposal.submit",
- error,
- EnvelopeContext::new("req_order", true),
- );
-
- assert_eq!(envelope.next_actions.len(), 2);
- assert_eq!(envelope.next_actions[0].kind, NextActionKind::CliCommand);
- assert_eq!(envelope.next_actions[0].label, "trade list");
- assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some("radroots trade list")
- );
- assert_eq!(envelope.next_actions[1].kind, NextActionKind::CliCommand);
- assert_eq!(envelope.next_actions[1].label, "basket create");
- assert_eq!(
- envelope.next_actions[1].command.as_deref(),
- Some("radroots basket create")
- );
- }
-
- #[test]
- fn failure_envelope_derives_operator_config_next_actions() {
- let mut error = OutputError::new(
- "operation_unavailable",
- "publish transport needs operator configuration",
- CliExitCode::RuntimeUnavailable,
- );
- error.detail = Some(json!({
- "actions": [
- "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID",
- "configure signer.remote_nip46 signer_session_ref",
- "configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID"
- ]
- }));
- let envelope = OutputEnvelope::failure(
- "transport.config.inspect",
- error,
- EnvelopeContext::new("req_config", false),
- );
- let value = serde_json::to_value(&envelope).expect("serialize envelope");
-
- assert_eq!(envelope.next_actions.len(), 2);
- assert_eq!(
- envelope.next_actions[0].kind,
- NextActionKind::OperatorConfig
- );
- assert_eq!(
- envelope.next_actions[0].label,
- "configure radrootsd execution token source"
- );
- assert_eq!(envelope.next_actions[0].command, None);
- assert_eq!(
- envelope.next_actions[0].env_var.as_deref(),
- Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE")
- );
- assert_eq!(
- envelope.next_actions[1].kind,
- NextActionKind::OperatorConfig
- );
- assert_eq!(
- envelope.next_actions[1].label,
- "configure signer session binding"
- );
- assert_eq!(envelope.next_actions[1].command, None);
- assert_eq!(
- envelope.next_actions[1].config_key.as_deref(),
- Some("signer.remote_nip46.signer_session_ref")
- );
- assert_eq!(value["next_actions"][0]["kind"], "operator_config");
- assert_eq!(value["next_actions"][0]["command"], Value::Null);
- assert_eq!(value["next_actions"][1]["kind"], "operator_config");
- assert_eq!(value["next_actions"][1]["command"], Value::Null);
- }
-
- #[test]
- fn ndjson_frames_serialize_one_json_object_per_line() {
- let frames = [
- NdjsonFrame::new(
- "sync.status",
- "req_watch",
- 0,
- NdjsonFrameType::Started,
- json!({ "state": "started" }),
- ),
- NdjsonFrame::new(
- "sync.status",
- "req_watch",
- 1,
- NdjsonFrameType::Event,
- json!({ "state": "submitted" }),
- ),
- NdjsonFrame::new(
- "sync.status",
- "req_watch",
- 2,
- NdjsonFrameType::Completed,
- json!({ "state": "complete" }),
- ),
- ];
- let rendered = frames
- .iter()
- .map(|frame| serde_json::to_string(frame).expect("serialize frame"))
- .collect::<Vec<_>>()
- .join("\n");
-
- for line in rendered.lines() {
- let value: Value = serde_json::from_str(line).expect("line is json");
- assert_eq!(value["schema_version"], OUTPUT_SCHEMA_VERSION);
- assert_eq!(value["operation_id"], "sync.status");
- assert!(value["frame_type"].is_string());
- }
- }
-
- #[test]
- fn ndjson_terminal_frame_carries_status_reason_and_resource() {
- let mut error = OutputError::new(
- "not_implemented",
- "operation is not implemented",
- CliExitCode::RuntimeUnavailable,
- );
- error.detail = Some(json!({
- "order_id": "ord_test",
- }));
- let envelope = OutputEnvelope::failure(
- "test.operation",
- error,
- EnvelopeContext::new("req_test", false),
- );
- let frames = envelope.to_ndjson_frames();
-
- assert_eq!(frames[0].payload["status"], "error");
- assert_eq!(frames[0].payload["output_format"], "terminal");
- assert_eq!(frames[1].payload["status"], "error");
- assert_eq!(frames[1].payload["reason_code"], "not_implemented");
- assert_eq!(frames[1].payload["resource"]["kind"], "trade");
- assert_eq!(frames[1].payload["resource"]["id"], "ord_test");
- assert_eq!(frames[1].errors[0].reason_code, "not_implemented");
- }
-
- #[test]
- fn exit_code_contract_matches_handoff_range() {
- assert_eq!(CliExitCode::Success.code(), 0);
- assert_eq!(CliExitCode::InvalidInput.code(), 2);
- assert_eq!(CliExitCode::ApprovalRequiredOrDenied.code(), 6);
- assert_eq!(CliExitCode::UnsafeOperationRefused.code(), 11);
- }
-}
diff --git a/src/out/mod.rs b/src/out/mod.rs
@@ -1,2 +0,0 @@
-pub mod envelope;
-pub mod terminal;
diff --git a/src/out/terminal/actions.rs b/src/out/terminal/actions.rs
@@ -1,328 +0,0 @@
-use crate::out::envelope::{NextAction, NextActionKind};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TerminalActionKind {
- Command,
- Setup,
- Placeholder,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalAction {
- pub label: Option<String>,
- pub command: Option<String>,
- pub description: Option<String>,
- pub kind: TerminalActionKind,
-}
-
-impl TerminalAction {
- pub fn command(command: impl Into<String>) -> Self {
- Self {
- label: None,
- command: Some(command.into()),
- description: None,
- kind: TerminalActionKind::Command,
- }
- }
-
- pub fn setup(description: impl Into<String>) -> Self {
- Self {
- label: None,
- command: None,
- description: Some(description.into()),
- kind: TerminalActionKind::Setup,
- }
- }
-
- pub fn placeholder(command: impl Into<String>) -> Self {
- Self {
- label: None,
- command: Some(command.into()),
- description: None,
- kind: TerminalActionKind::Placeholder,
- }
- }
-
- pub fn render_line(&self) -> Option<String> {
- self.command
- .clone()
- .or_else(|| self.description.clone())
- .or_else(|| self.label.clone())
- }
-}
-
-pub fn terminal_actions_from_next_actions(actions: &[NextAction]) -> Vec<TerminalAction> {
- actions
- .iter()
- .filter_map(terminal_action_from_next_action)
- .fold(Vec::<TerminalAction>::new(), |mut unique, action| {
- if !unique.contains(&action) {
- unique.push(action);
- }
- unique
- })
-}
-
-fn terminal_action_from_next_action(action: &NextAction) -> Option<TerminalAction> {
- match action.kind {
- NextActionKind::CliCommand => action
- .command
- .as_deref()
- .map(terminal_action_from_command)
- .or_else(|| action.description.clone().map(TerminalAction::setup)),
- NextActionKind::OperatorConfig => action
- .description
- .clone()
- .map(TerminalAction::setup)
- .or_else(|| Some(TerminalAction::setup(action.label.clone()))),
- }
-}
-
-pub fn terminal_action_from_command(command: &str) -> TerminalAction {
- let command = command.trim();
- if command_requires_placeholder(command) {
- TerminalAction::placeholder(command_with_placeholder(command))
- } else {
- TerminalAction::command(command)
- }
-}
-
-fn command_requires_placeholder(command: &str) -> bool {
- matches!(command, "radroots listing publish")
- || basket_item_add_requires_placeholder(command)
- || basket_item_update_requires_placeholder(command)
- || basket_adjustment_add_requires_placeholder(command)
- || basket_adjustment_remove_requires_placeholder(command)
- || basket_quote_create_requires_placeholder(command)
-}
-
-fn command_with_placeholder(command: &str) -> String {
- if basket_item_add_requires_placeholder(command) {
- return basket_item_add_placeholder(command);
- }
- if basket_item_update_requires_placeholder(command) {
- return basket_item_update_placeholder(command);
- }
- if basket_adjustment_add_requires_placeholder(command) {
- return basket_adjustment_add_placeholder(command);
- }
- if basket_adjustment_remove_requires_placeholder(command) {
- return basket_adjustment_remove_placeholder(command);
- }
- if basket_quote_create_requires_placeholder(command) {
- return basket_quote_create_placeholder(command);
- }
- match command {
- "radroots listing publish" => "radroots listing publish <file>",
- other => other,
- }
- .to_owned()
-}
-
-fn basket_item_add_requires_placeholder(command: &str) -> bool {
- command_has_prefix(command, &["radroots", "basket", "item", "add"])
- && (!has_basket_arg(command, 4)
- || (!has_flag(command, "--listing") && !has_flag(command, "--listing-addr"))
- || !has_flag(command, "--bin-id"))
-}
-
-fn basket_item_update_requires_placeholder(command: &str) -> bool {
- command_has_prefix(command, &["radroots", "basket", "item", "update"])
- && (!has_basket_arg(command, 4)
- || !has_flag(command, "--item-id")
- || !has_any_flag(
- command,
- &["--listing", "--listing-addr", "--bin-id", "--quantity"],
- ))
-}
-
-fn basket_adjustment_add_requires_placeholder(command: &str) -> bool {
- command_has_prefix(command, &["radroots", "basket", "adjustment", "add"])
- && (!has_basket_arg(command, 4)
- || !has_flag(command, "--id")
- || !has_flag(command, "--effect")
- || !has_flag(command, "--amount")
- || !has_flag(command, "--currency")
- || !has_flag(command, "--reason"))
-}
-
-fn basket_adjustment_remove_requires_placeholder(command: &str) -> bool {
- command_has_prefix(command, &["radroots", "basket", "adjustment", "remove"])
- && (!has_basket_arg(command, 4) || !has_flag(command, "--id"))
-}
-
-fn basket_quote_create_requires_placeholder(command: &str) -> bool {
- command_has_prefix(command, &["radroots", "basket", "quote", "create"])
- && !has_basket_arg(command, 4)
-}
-
-fn basket_item_add_placeholder(command: &str) -> String {
- let basket = basket_arg(command, 4);
- let listing = flag_value(command, "--listing")
- .map(|value| format!("--listing {value}"))
- .or_else(|| {
- flag_value(command, "--listing-addr").map(|value| format!("--listing-addr {value}"))
- })
- .unwrap_or_else(|| "--listing <product>".to_owned());
- let bin = flag_value(command, "--bin-id").unwrap_or_else(|| "<bin>".to_owned());
- let quantity = flag_value(command, "--quantity")
- .map(|value| format!(" --quantity {value}"))
- .unwrap_or_default();
- format!("radroots basket item add {basket} {listing} --bin-id {bin}{quantity}")
-}
-
-fn basket_item_update_placeholder(command: &str) -> String {
- let basket = basket_arg(command, 4);
- let item = flag_value(command, "--item-id").unwrap_or_else(|| "<item>".to_owned());
- let update = if let Some(value) = flag_value(command, "--listing") {
- format!("--listing {value}")
- } else if let Some(value) = flag_value(command, "--listing-addr") {
- format!("--listing-addr {value}")
- } else if let Some(value) = flag_value(command, "--bin-id") {
- format!("--bin-id {value}")
- } else if let Some(value) = flag_value(command, "--quantity") {
- format!("--quantity {value}")
- } else {
- "--quantity <quantity>".to_owned()
- };
- format!("radroots basket item update {basket} --item-id {item} {update}")
-}
-
-fn basket_adjustment_add_placeholder(command: &str) -> String {
- let basket = basket_arg(command, 4);
- let id = flag_value(command, "--id").unwrap_or_else(|| "<id>".to_owned());
- let effect = flag_value(command, "--effect").unwrap_or_else(|| "<effect>".to_owned());
- let amount = flag_value(command, "--amount").unwrap_or_else(|| "<amount>".to_owned());
- let currency = flag_value(command, "--currency").unwrap_or_else(|| "<currency>".to_owned());
- let reason = flag_value(command, "--reason").unwrap_or_else(|| "<reason>".to_owned());
- format!(
- "radroots basket adjustment add {basket} --id {id} --effect {effect} --amount {amount} --currency {currency} --reason {reason}"
- )
-}
-
-fn basket_adjustment_remove_placeholder(command: &str) -> String {
- let basket = basket_arg(command, 4);
- let id = flag_value(command, "--id").unwrap_or_else(|| "<id>".to_owned());
- format!("radroots basket adjustment remove {basket} --id {id}")
-}
-
-fn basket_quote_create_placeholder(command: &str) -> String {
- let basket = basket_arg(command, 4);
- format!("radroots basket quote create {basket}")
-}
-
-fn command_has_prefix(command: &str, prefix: &[&str]) -> bool {
- let parts = command.split_whitespace().collect::<Vec<_>>();
- parts.len() >= prefix.len()
- && parts
- .iter()
- .zip(prefix.iter())
- .all(|(part, prefix)| part == prefix)
-}
-
-fn has_basket_arg(command: &str, index: usize) -> bool {
- command
- .split_whitespace()
- .nth(index)
- .is_some_and(|value| !value.starts_with("--"))
-}
-
-fn basket_arg(command: &str, index: usize) -> String {
- command
- .split_whitespace()
- .nth(index)
- .filter(|value| !value.starts_with("--"))
- .unwrap_or("<basket>")
- .to_owned()
-}
-
-fn has_any_flag(command: &str, flags: &[&str]) -> bool {
- flags.iter().any(|flag| has_flag(command, flag))
-}
-
-fn has_flag(command: &str, flag: &str) -> bool {
- flag_value(command, flag).is_some()
-}
-
-fn flag_value(command: &str, flag: &str) -> Option<String> {
- let mut parts = command.split_whitespace();
- while let Some(part) = parts.next() {
- if part == flag {
- return parts
- .next()
- .filter(|value| !value.starts_with("--"))
- .map(str::to_owned);
- }
- }
- None
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::out::envelope::NextAction;
-
- #[test]
- fn exact_command_renders_as_command() {
- let action = terminal_action_from_command("radroots store inspect");
-
- assert_eq!(action.kind, TerminalActionKind::Command);
- assert_eq!(action.command.as_deref(), Some("radroots store inspect"));
- }
-
- #[test]
- fn incomplete_command_is_placeholdered() {
- let action = terminal_action_from_command("radroots basket item add");
-
- assert_eq!(action.kind, TerminalActionKind::Placeholder);
- assert_eq!(
- action.command.as_deref(),
- Some("radroots basket item add <basket> --listing <product> --bin-id <bin>")
- );
- }
-
- #[test]
- fn partially_bound_basket_item_add_is_placeholdered() {
- let action = terminal_action_from_command("radroots basket item add basket_test");
-
- assert_eq!(action.kind, TerminalActionKind::Placeholder);
- assert_eq!(
- action.command.as_deref(),
- Some("radroots basket item add basket_test --listing <product> --bin-id <bin>")
- );
- }
-
- #[test]
- fn complete_basket_item_add_renders_as_command() {
- let action = terminal_action_from_command(
- "radroots basket item add basket_test --listing eggs --bin-id bin-1",
- );
-
- assert_eq!(action.kind, TerminalActionKind::Command);
- assert_eq!(
- action.command.as_deref(),
- Some("radroots basket item add basket_test --listing eggs --bin-id bin-1")
- );
- }
-
- #[test]
- fn converts_operator_config_action_to_setup() {
- let action = NextAction {
- kind: NextActionKind::OperatorConfig,
- label: "configure token".to_owned(),
- command: None,
- description: Some("configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()),
- env_var: Some("RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE".to_owned()),
- config_key: None,
- };
-
- let terminal = terminal_actions_from_next_actions(&[action]);
-
- assert_eq!(terminal.len(), 1);
- assert_eq!(terminal[0].kind, TerminalActionKind::Setup);
- assert_eq!(
- terminal[0].description.as_deref(),
- Some("configure RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE")
- );
- }
-}
diff --git a/src/out/terminal/errors.rs b/src/out/terminal/errors.rs
@@ -1,55 +0,0 @@
-use crate::out::envelope::{OutputEnvelope, OutputError};
-
-use super::actions::terminal_actions_from_next_actions;
-use super::layout::{TerminalDocument, TerminalField, TerminalHeader, TerminalSymbol};
-
-pub fn terminal_error_document(envelope: &OutputEnvelope) -> TerminalDocument {
- let error = envelope.errors.first();
- let title = error
- .map(error_title)
- .unwrap_or_else(|| "Command failed".to_owned());
- let mut document = TerminalDocument::new(TerminalHeader::new(TerminalSymbol::Failure, title));
- if let Some(error) = error {
- document
- .fields
- .push(TerminalField::essential("Reason", error.message.clone()));
- document
- .fields
- .push(TerminalField::verbose("Code", error.reason_code.clone()));
- }
- document.next = terminal_actions_from_next_actions(&envelope.next_actions);
- document
-}
-
-fn error_title(error: &OutputError) -> String {
- match error.code.as_str() {
- "not_found" => "Not found",
- "invalid_input" => "Invalid input",
- "approval_required" => "Approval required",
- "operation_unavailable" => "Unavailable",
- _ => "Command failed",
- }
- .to_owned()
-}
-
-#[cfg(test)]
-mod tests {
- use crate::out::envelope::{CliExitCode, EnvelopeContext, OutputEnvelope, OutputError};
-
- use super::*;
-
- #[test]
- fn builds_terminal_error_document() {
- let error = OutputError::new("invalid_input", "missing input", CliExitCode::InvalidInput);
- let envelope = OutputEnvelope::failure(
- "trade.proposal.submit",
- error,
- EnvelopeContext::new("req_test", false),
- );
- let document = terminal_error_document(&envelope);
-
- assert_eq!(document.header.title, "Invalid input");
- assert_eq!(document.fields[0].label, "Reason");
- assert_eq!(document.fields[0].value, "missing input");
- }
-}
diff --git a/src/out/terminal/layout.rs b/src/out/terminal/layout.rs
@@ -1,231 +0,0 @@
-use super::actions::TerminalAction;
-use super::tables::TerminalTable;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TerminalSymbol {
- Success,
- Neutral,
- Attention,
- Failure,
-}
-
-impl TerminalSymbol {
- pub fn glyph(self) -> &'static str {
- match self {
- Self::Success => "✓",
- Self::Neutral => "◌",
- Self::Attention => "!",
- Self::Failure => "✕",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TerminalVisibility {
- Essential,
- Normal,
- Verbose,
- Trace,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalHeader {
- pub symbol: TerminalSymbol,
- pub title: String,
-}
-
-impl TerminalHeader {
- pub fn new(symbol: TerminalSymbol, title: impl Into<String>) -> Self {
- Self {
- symbol,
- title: title.into(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalField {
- pub label: String,
- pub value: String,
- pub visibility: TerminalVisibility,
-}
-
-impl TerminalField {
- pub fn essential(label: impl Into<String>, value: impl Into<String>) -> Self {
- Self {
- label: label.into(),
- value: value.into(),
- visibility: TerminalVisibility::Essential,
- }
- }
-
- pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
- Self {
- label: label.into(),
- value: value.into(),
- visibility: TerminalVisibility::Normal,
- }
- }
-
- pub fn verbose(label: impl Into<String>, value: impl Into<String>) -> Self {
- Self {
- label: label.into(),
- value: value.into(),
- visibility: TerminalVisibility::Verbose,
- }
- }
-
- pub fn trace(label: impl Into<String>, value: impl Into<String>) -> Self {
- Self {
- label: label.into(),
- value: value.into(),
- visibility: TerminalVisibility::Trace,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum TerminalSectionBody {
- Lines(Vec<String>),
- Fields(Vec<TerminalField>),
- Table(TerminalTable),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalSection {
- pub title: String,
- pub body: TerminalSectionBody,
- pub visibility: TerminalVisibility,
-}
-
-impl TerminalSection {
- pub fn lines(title: impl Into<String>, lines: Vec<String>) -> Self {
- Self {
- title: title.into(),
- body: TerminalSectionBody::Lines(lines),
- visibility: TerminalVisibility::Normal,
- }
- }
-
- pub fn fields(title: impl Into<String>, fields: Vec<TerminalField>) -> Self {
- Self {
- title: title.into(),
- body: TerminalSectionBody::Fields(fields),
- visibility: TerminalVisibility::Normal,
- }
- }
-
- pub fn table(title: impl Into<String>, table: TerminalTable) -> Self {
- Self {
- title: title.into(),
- body: TerminalSectionBody::Table(table),
- visibility: TerminalVisibility::Normal,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalWarning {
- pub code: String,
- pub message: String,
-}
-
-impl TerminalWarning {
- pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
- Self {
- code: code.into(),
- message: message.into(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct TerminalReference {
- pub request_id: Option<String>,
- pub correlation_id: Option<String>,
- pub idempotency_key: Option<String>,
- pub event_id: Option<String>,
- pub event_addr: Option<String>,
- pub job_id: Option<String>,
- pub path: Option<String>,
- pub source: Option<String>,
-}
-
-impl TerminalReference {
- pub fn is_empty(&self) -> bool {
- self.request_id.is_none()
- && self.correlation_id.is_none()
- && self.idempotency_key.is_none()
- && self.event_id.is_none()
- && self.event_addr.is_none()
- && self.job_id.is_none()
- && self.path.is_none()
- && self.source.is_none()
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalDocument {
- pub header: TerminalHeader,
- pub fields: Vec<TerminalField>,
- pub sections: Vec<TerminalSection>,
- pub warnings: Vec<TerminalWarning>,
- pub next: Vec<TerminalAction>,
- pub reference: Option<TerminalReference>,
-}
-
-impl TerminalDocument {
- pub fn new(header: TerminalHeader) -> Self {
- Self {
- header,
- fields: Vec::new(),
- sections: Vec::new(),
- warnings: Vec::new(),
- next: Vec::new(),
- reference: None,
- }
- }
-
- pub fn with_field(mut self, field: TerminalField) -> Self {
- self.fields.push(field);
- self
- }
-
- pub fn with_section(mut self, section: TerminalSection) -> Self {
- self.sections.push(section);
- self
- }
-
- pub fn with_next(mut self, action: TerminalAction) -> Self {
- self.next.push(action);
- self
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn symbols_match_terminal_contract() {
- assert_eq!(TerminalSymbol::Success.glyph(), "✓");
- assert_eq!(TerminalSymbol::Neutral.glyph(), "◌");
- assert_eq!(TerminalSymbol::Attention.glyph(), "!");
- assert_eq!(TerminalSymbol::Failure.glyph(), "✕");
- }
-
- #[test]
- fn document_defaults_to_empty_body() {
- let document = TerminalDocument::new(TerminalHeader::new(
- TerminalSymbol::Success,
- "Workspace ready",
- ));
-
- assert_eq!(document.header.title, "Workspace ready");
- assert!(document.fields.is_empty());
- assert!(document.sections.is_empty());
- assert!(document.warnings.is_empty());
- assert!(document.next.is_empty());
- assert!(document.reference.is_none());
- }
-}
diff --git a/src/out/terminal/mod.rs b/src/out/terminal/mod.rs
@@ -1,8 +0,0 @@
-pub mod actions;
-pub mod errors;
-pub mod layout;
-pub mod registry;
-pub mod renderer;
-pub mod renderers;
-pub mod tables;
-pub mod values;
diff --git a/src/out/terminal/registry.rs b/src/out/terminal/registry.rs
@@ -1,340 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-use std::fmt;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::registry::OperationSpec;
-
-use super::layout::TerminalDocument;
-use super::renderer::TerminalRenderContext;
-
-pub trait TerminalOperationRenderer {
- fn render(&self, envelope: &OutputEnvelope, cx: &TerminalRenderContext) -> TerminalDocument;
-}
-
-#[derive(Default)]
-pub struct TerminalRendererRegistry {
- entries: Vec<TerminalRendererEntry>,
- violations: Vec<TerminalRendererRegistryViolation>,
-}
-
-struct TerminalRendererEntry {
- operation_id: &'static str,
- renderer: &'static dyn TerminalOperationRenderer,
-}
-
-impl TerminalRendererRegistry {
- pub fn new() -> Self {
- Self {
- entries: Vec::new(),
- violations: Vec::new(),
- }
- }
-
- pub fn register(
- mut self,
- operation_id: &'static str,
- renderer: &'static dyn TerminalOperationRenderer,
- ) -> Self {
- if self.contains(operation_id) {
- self.violations
- .push(TerminalRendererRegistryViolation::duplicate(operation_id));
- }
- self.entries.push(TerminalRendererEntry {
- operation_id,
- renderer,
- });
- self
- }
-
- pub fn contains(&self, operation_id: &str) -> bool {
- self.entries
- .iter()
- .any(|entry| entry.operation_id == operation_id)
- }
-
- pub fn get(&self, operation_id: &str) -> Option<&'static dyn TerminalOperationRenderer> {
- let mut matches = self
- .entries
- .iter()
- .filter(|entry| entry.operation_id == operation_id);
- let renderer = matches.next()?.renderer;
- matches.next().is_none().then_some(renderer)
- }
-
- pub fn len(&self) -> usize {
- self.entries.len()
- }
-
- pub fn is_empty(&self) -> bool {
- self.entries.is_empty()
- }
-
- pub fn operation_ids(&self) -> impl Iterator<Item = &'static str> + '_ {
- self.entries.iter().map(|entry| entry.operation_id)
- }
-
- pub fn renderer_for(
- &self,
- operation_id: &str,
- ) -> Result<&'static dyn TerminalOperationRenderer, TerminalRendererRegistryError> {
- self.get(operation_id).ok_or_else(|| {
- TerminalRendererRegistryError::from_violation(
- TerminalRendererRegistryViolation::missing(operation_id.to_owned()),
- )
- })
- }
-
- pub fn validate_against_operations(
- &self,
- operations: &[OperationSpec],
- ) -> Result<(), TerminalRendererRegistryError> {
- self.validate_operation_ids(operations.iter().map(|operation| operation.operation_id))
- }
-
- pub fn validate_operation_ids<'a>(
- &self,
- expected: impl IntoIterator<Item = &'a str>,
- ) -> Result<(), TerminalRendererRegistryError> {
- let expected = expected.into_iter().collect::<BTreeSet<_>>();
- let mut counts = BTreeMap::<&str, usize>::new();
- for operation_id in self.operation_ids() {
- *counts.entry(operation_id).or_default() += 1;
- }
- let mut violations = self.violations.clone();
- for operation_id in &expected {
- match counts.get(operation_id).copied().unwrap_or(0) {
- 0 => {
- violations.push(TerminalRendererRegistryViolation::missing(
- (*operation_id).to_owned(),
- ));
- }
- 1 => {}
- count => {
- violations.push(TerminalRendererRegistryViolation::duplicate_count(
- operation_id.to_owned(),
- count,
- ));
- }
- }
- }
- for operation_id in counts.keys() {
- if !expected.contains(operation_id) {
- violations.push(TerminalRendererRegistryViolation::extra(*operation_id));
- }
- }
- violations.sort();
- violations.dedup();
- if violations.is_empty() {
- Ok(())
- } else {
- Err(TerminalRendererRegistryError { violations })
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
-pub enum TerminalRendererRegistryViolation {
- DuplicateOperation { operation_id: String, count: usize },
- ExtraOperation { operation_id: String },
- MissingOperation { operation_id: String },
-}
-
-impl TerminalRendererRegistryViolation {
- fn duplicate(operation_id: impl Into<String>) -> Self {
- Self::DuplicateOperation {
- operation_id: operation_id.into(),
- count: 2,
- }
- }
-
- fn duplicate_count(operation_id: impl Into<String>, count: usize) -> Self {
- Self::DuplicateOperation {
- operation_id: operation_id.into(),
- count,
- }
- }
-
- fn extra(operation_id: impl Into<String>) -> Self {
- Self::ExtraOperation {
- operation_id: operation_id.into(),
- }
- }
-
- fn missing(operation_id: impl Into<String>) -> Self {
- Self::MissingOperation {
- operation_id: operation_id.into(),
- }
- }
-
- pub fn kind(&self) -> &'static str {
- match self {
- Self::DuplicateOperation { .. } => "duplicate",
- Self::ExtraOperation { .. } => "extra",
- Self::MissingOperation { .. } => "missing",
- }
- }
-
- pub fn operation_id(&self) -> &str {
- match self {
- Self::DuplicateOperation { operation_id, .. }
- | Self::ExtraOperation { operation_id }
- | Self::MissingOperation { operation_id } => operation_id,
- }
- }
-
- pub fn count(&self) -> Option<usize> {
- match self {
- Self::DuplicateOperation { count, .. } => Some(*count),
- Self::ExtraOperation { .. } | Self::MissingOperation { .. } => None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalRendererRegistryError {
- violations: Vec<TerminalRendererRegistryViolation>,
-}
-
-impl TerminalRendererRegistryError {
- pub fn from_violation(violation: TerminalRendererRegistryViolation) -> Self {
- Self {
- violations: vec![violation],
- }
- }
-
- pub fn violations(&self) -> &[TerminalRendererRegistryViolation] {
- &self.violations
- }
-}
-
-impl fmt::Display for TerminalRendererRegistryError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str("terminal renderer registry invariant failed")?;
- for violation in &self.violations {
- match violation {
- TerminalRendererRegistryViolation::DuplicateOperation {
- operation_id,
- count,
- } => write!(
- formatter,
- "; duplicate renderer for {operation_id} ({count})"
- )?,
- TerminalRendererRegistryViolation::ExtraOperation { operation_id } => {
- write!(formatter, "; unexpected renderer for {operation_id}")?
- }
- TerminalRendererRegistryViolation::MissingOperation { operation_id } => {
- write!(formatter, "; missing renderer for {operation_id}")?
- }
- }
- }
- Ok(())
- }
-}
-
-pub fn terminal_renderer_registry() -> TerminalRendererRegistry {
- let registry = TerminalRendererRegistry::new();
- crate::out::terminal::renderers::v1::register(registry)
-}
-
-#[cfg(test)]
-mod tests {
- use crate::out::terminal::layout::{TerminalDocument, TerminalHeader, TerminalSymbol};
- use crate::registry::OPERATION_REGISTRY;
-
- use super::*;
-
- struct TestRenderer;
-
- impl TerminalOperationRenderer for TestRenderer {
- fn render(
- &self,
- _envelope: &OutputEnvelope,
- _cx: &TerminalRenderContext,
- ) -> TerminalDocument {
- TerminalDocument::new(TerminalHeader::new(TerminalSymbol::Success, "ok"))
- }
- }
-
- static TEST_RENDERER: TestRenderer = TestRenderer;
-
- #[test]
- fn registers_operation_renderers() {
- let registry = TerminalRendererRegistry::new().register("profile.inspect", &TEST_RENDERER);
-
- assert!(registry.contains("profile.inspect"));
- assert_eq!(registry.len(), 1);
- assert!(registry.get("profile.inspect").is_some());
- assert!(registry.get("missing").is_none());
- }
-
- #[test]
- fn duplicate_operation_renderer_registration_fails() {
- let registry = TerminalRendererRegistry::new()
- .register("profile.inspect", &TEST_RENDERER)
- .register("profile.inspect", &TEST_RENDERER);
- let error = registry
- .validate_operation_ids(["profile.inspect"])
- .expect_err("duplicate renderer registration should fail");
-
- assert_eq!(
- error.violations(),
- &[TerminalRendererRegistryViolation::duplicate_count(
- "profile.inspect",
- 2
- )]
- );
- assert!(registry.get("profile.inspect").is_none());
- }
-
- #[test]
- fn missing_operation_renderer_registration_fails() {
- let error = TerminalRendererRegistry::new()
- .validate_operation_ids(["profile.inspect"])
- .expect_err("missing renderer registration should fail");
-
- assert_eq!(
- error.violations(),
- &[TerminalRendererRegistryViolation::missing(
- "profile.inspect"
- )]
- );
- }
-
- #[test]
- fn extra_operation_renderer_registration_fails() {
- let error = TerminalRendererRegistry::new()
- .register("test.extra", &TEST_RENDERER)
- .validate_operation_ids(["profile.inspect"])
- .expect_err("extra renderer registration should fail");
-
- assert_eq!(
- error.violations(),
- &[
- TerminalRendererRegistryViolation::extra("test.extra"),
- TerminalRendererRegistryViolation::missing("profile.inspect"),
- ]
- );
- }
-
- #[test]
- fn registry_covers_registered_operations() {
- let registry = terminal_renderer_registry();
- let expected = OPERATION_REGISTRY
- .iter()
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let actual = registry.operation_ids().collect::<BTreeSet<_>>();
-
- assert_eq!(registry.len(), OPERATION_REGISTRY.len());
- assert_eq!(actual, expected);
- registry
- .validate_against_operations(OPERATION_REGISTRY)
- .expect("terminal registry covers operation registry");
- for operation_id in expected {
- assert!(
- registry.contains(operation_id),
- "missing terminal renderer for {operation_id}"
- );
- }
- }
-}
diff --git a/src/out/terminal/renderer.rs b/src/out/terminal/renderer.rs
@@ -1,276 +0,0 @@
-use std::fmt::Write as _;
-
-use super::actions::TerminalAction;
-use super::layout::{
- TerminalDocument, TerminalField, TerminalReference, TerminalSection, TerminalSectionBody,
- TerminalVisibility,
-};
-use super::tables::{TerminalTable, truncate_cell};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TerminalVerbosity {
- Quiet,
- Normal,
- Verbose,
- Trace,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalRenderContext {
- pub verbosity: TerminalVerbosity,
- pub width: usize,
- pub dry_run: bool,
-}
-
-impl Default for TerminalRenderContext {
- fn default() -> Self {
- Self {
- verbosity: TerminalVerbosity::Normal,
- width: 80,
- dry_run: false,
- }
- }
-}
-
-impl TerminalRenderContext {
- pub fn is_quiet(&self) -> bool {
- matches!(self.verbosity, TerminalVerbosity::Quiet)
- }
-
- pub fn allows(&self, visibility: TerminalVisibility) -> bool {
- match visibility {
- TerminalVisibility::Essential => true,
- TerminalVisibility::Normal => !self.is_quiet(),
- TerminalVisibility::Verbose => {
- matches!(
- self.verbosity,
- TerminalVerbosity::Verbose | TerminalVerbosity::Trace
- )
- }
- TerminalVisibility::Trace => matches!(self.verbosity, TerminalVerbosity::Trace),
- }
- }
-}
-
-pub fn render_terminal_document(document: &TerminalDocument, cx: &TerminalRenderContext) -> String {
- let mut output = String::new();
- let _ = writeln!(
- output,
- "{} {}",
- document.header.symbol.glyph(),
- document.header.title
- );
- write_fields(&mut output, &document.fields, cx);
- for section in &document.sections {
- write_section(&mut output, section, cx);
- }
- if !cx.is_quiet() && !document.warnings.is_empty() {
- let _ = writeln!(output);
- let _ = writeln!(output, "Warnings");
- for warning in &document.warnings {
- let _ = writeln!(output, " {}: {}", warning.code, warning.message);
- }
- }
- if !cx.is_quiet() {
- write_next(&mut output, &document.next);
- }
- if let Some(reference) = &document.reference
- && cx.allows(TerminalVisibility::Verbose)
- && !reference.is_empty()
- {
- write_reference(&mut output, reference);
- }
- output.trim_end_matches('\n').to_owned()
-}
-
-fn write_fields(output: &mut String, fields: &[TerminalField], cx: &TerminalRenderContext) {
- let fields = fields
- .iter()
- .filter(|field| cx.allows(field.visibility))
- .collect::<Vec<_>>();
- if fields.is_empty() {
- return;
- }
- let width = fields
- .iter()
- .map(|field| field.label.chars().count())
- .max()
- .unwrap_or(0);
- let _ = writeln!(output);
- for field in fields {
- let _ = writeln!(
- output,
- " {label:<width$} {value}",
- label = field.label,
- width = width,
- value = field.value
- );
- }
-}
-
-fn write_section(output: &mut String, section: &TerminalSection, cx: &TerminalRenderContext) {
- if !cx.allows(section.visibility) {
- return;
- }
- let _ = writeln!(output);
- let _ = writeln!(output, "{}", section.title);
- match §ion.body {
- TerminalSectionBody::Lines(lines) => {
- for line in lines {
- let _ = writeln!(output, " {line}");
- }
- }
- TerminalSectionBody::Fields(fields) => write_fields(output, fields, cx),
- TerminalSectionBody::Table(table) => write_table(output, table, cx.width),
- }
-}
-
-fn write_table(output: &mut String, table: &TerminalTable, width: usize) {
- if table.rows.is_empty() {
- if let Some(empty) = &table.empty {
- let _ = writeln!(output, " {empty}");
- }
- return;
- }
- let widths = table.column_widths(width.saturating_sub(2));
- let labels = table
- .columns
- .iter()
- .zip(widths.iter())
- .map(|(column, width)| {
- format!(
- "{:<width$}",
- truncate_cell(&column.label, *width),
- width = *width
- )
- })
- .collect::<Vec<_>>()
- .join(" ");
- let _ = writeln!(output, " {labels}");
- for row in &table.rows {
- let line = widths
- .iter()
- .enumerate()
- .map(|(index, width)| {
- let cell = row.cells.get(index).map(String::as_str).unwrap_or("");
- format!("{:<width$}", truncate_cell(cell, *width), width = *width)
- })
- .collect::<Vec<_>>()
- .join(" ");
- let _ = writeln!(output, " {line}");
- }
-}
-
-fn write_next(output: &mut String, actions: &[TerminalAction]) {
- let visible = actions
- .iter()
- .filter_map(|action| action.render_line())
- .collect::<Vec<_>>();
- if visible.is_empty() {
- return;
- }
- let _ = writeln!(output);
- let _ = writeln!(output, "Next");
- for action in visible {
- let _ = writeln!(output, " {action}");
- }
-}
-
-fn write_reference(output: &mut String, reference: &TerminalReference) {
- let fields = [
- ("Request", reference.request_id.as_ref()),
- ("Correlation", reference.correlation_id.as_ref()),
- ("Idempotency", reference.idempotency_key.as_ref()),
- ("Event", reference.event_id.as_ref()),
- ("Address", reference.event_addr.as_ref()),
- ("Job", reference.job_id.as_ref()),
- ("Path", reference.path.as_ref()),
- ("Source", reference.source.as_ref()),
- ]
- .into_iter()
- .filter_map(|(label, value)| value.map(|value| TerminalField::new(label, value)))
- .collect::<Vec<_>>();
- let _ = writeln!(output);
- let _ = writeln!(output, "Reference");
- write_fields(output, &fields, &TerminalRenderContext::default());
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::out::terminal::actions::TerminalAction;
- use crate::out::terminal::layout::{
- TerminalField, TerminalHeader, TerminalSection, TerminalSymbol, TerminalWarning,
- };
-
- #[test]
- fn renders_canonical_receipt_layout() {
- let document = TerminalDocument::new(TerminalHeader::new(
- TerminalSymbol::Success,
- "Listing published",
- ))
- .with_field(TerminalField::new("Listing", "AAAAAAAAAAAAAAAAAAAAAg"))
- .with_field(TerminalField::new("Transport", "nostr"))
- .with_field(TerminalField::new("Targets", "2 acknowledged · 0 failed"))
- .with_field(TerminalField::new("Event", "9f3a…c12"))
- .with_next(TerminalAction::command(
- "radroots listing get AAAAAAAAAAAAAAAAAAAAAg",
- ));
-
- assert_eq!(
- render_terminal_document(&document, &TerminalRenderContext::default()),
- "✓ Listing published\n\n Listing AAAAAAAAAAAAAAAAAAAAAg\n Transport nostr\n Targets 2 acknowledged · 0 failed\n Event 9f3a…c12\n\nNext\n radroots listing get AAAAAAAAAAAAAAAAAAAAAg"
- );
- }
-
- #[test]
- fn hides_reference_until_verbose() {
- let mut document =
- TerminalDocument::new(TerminalHeader::new(TerminalSymbol::Success, "Ready"));
- document.reference = Some(TerminalReference {
- request_id: Some("req_test".to_owned()),
- ..TerminalReference::default()
- });
-
- assert_eq!(
- render_terminal_document(&document, &TerminalRenderContext::default()),
- "✓ Ready"
- );
-
- let cx = TerminalRenderContext {
- verbosity: TerminalVerbosity::Verbose,
- ..TerminalRenderContext::default()
- };
- assert!(render_terminal_document(&document, &cx).contains("Reference"));
- }
-
- #[test]
- fn quiet_renders_only_header_and_essential_fields() {
- let mut document = TerminalDocument::new(TerminalHeader::new(
- TerminalSymbol::Failure,
- "Command failed",
- ))
- .with_field(TerminalField::essential("Reason", "missing approval"))
- .with_field(TerminalField::new("State", "not implemented"))
- .with_section(TerminalSection::lines(
- "Details",
- vec!["extra context".to_owned()],
- ))
- .with_next(TerminalAction::command("radroots trade get ord_test"));
- document.warnings = vec![TerminalWarning::new("warn_test", "suppressed")];
- document.reference = Some(TerminalReference {
- request_id: Some("req_test".to_owned()),
- ..TerminalReference::default()
- });
-
- let rendered = render_terminal_document(
- &document,
- &TerminalRenderContext {
- verbosity: TerminalVerbosity::Quiet,
- ..TerminalRenderContext::default()
- },
- );
-
- assert_eq!(rendered, "✕ Command failed\n\n Reason missing approval");
- }
-}
diff --git a/src/out/terminal/renderers/common.rs b/src/out/terminal/renderers/common.rs
@@ -1,209 +0,0 @@
-use serde_json::Value;
-
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::actions::terminal_actions_from_next_actions;
-use crate::out::terminal::errors::terminal_error_document;
-use crate::out::terminal::layout::{
- TerminalDocument, TerminalField, TerminalHeader, TerminalReference, TerminalSymbol,
- TerminalWarning,
-};
-use crate::out::terminal::values::{proof_summary, string_path, transport_label};
-
-pub(crate) fn base_terminal_document(envelope: &OutputEnvelope) -> TerminalDocument {
- let display = terminal_display_source(envelope);
- let mut document = if envelope.errors.is_empty() {
- let status = terminal_envelope_status(envelope);
- TerminalDocument::new(TerminalHeader::new(
- terminal_status_symbol(status, envelope.dry_run),
- terminal_title(envelope.operation_id.as_str(), status),
- ))
- } else {
- let mut document = terminal_error_document(envelope);
- add_terminal_display_fields(&mut document, display, false);
- document
- };
- if envelope.errors.is_empty() {
- add_terminal_display_fields(&mut document, display, true);
- }
- document.warnings = envelope
- .warnings
- .iter()
- .map(|warning| TerminalWarning::new(warning.code.clone(), warning.message.clone()))
- .collect();
- document.next = terminal_actions_from_next_actions(&envelope.next_actions);
- document.reference = terminal_reference(envelope);
- document
-}
-
-fn push_field(document: &mut TerminalDocument, label: impl Into<String>, value: impl Into<String>) {
- let label = label.into();
- let value = value.into();
- if value.trim().is_empty() {
- return;
- }
- if document
- .fields
- .iter()
- .any(|field| field.label == label && field.value == value)
- {
- return;
- }
- document.fields.push(TerminalField::new(label, value));
-}
-
-fn add_terminal_display_fields(
- document: &mut TerminalDocument,
- display: &Value,
- include_reason: bool,
-) {
- if let Some(state) = terminal_state(display) {
- push_field(document, "State", terminal_status_label(state));
- }
- if let Some(mode) = terminal_transport_profile(display) {
- push_field(document, "Transport", transport_label(mode));
- }
- if let Some(state) = terminal_publish_state(display) {
- push_field(document, "Publish", terminal_status_label(state));
- }
- if let Some(proof) = proof_summary(display) {
- push_field(document, "Proof", proof);
- }
- if include_reason && let Some(reason) = terminal_reason(display) {
- push_field(document, "Reason", reason.to_owned());
- }
-}
-
-fn terminal_reference(envelope: &OutputEnvelope) -> Option<TerminalReference> {
- let reference = TerminalReference {
- request_id: Some(envelope.request_id.clone()),
- correlation_id: envelope.correlation_id.clone(),
- idempotency_key: envelope.idempotency_key.clone(),
- event_id: None,
- event_addr: None,
- job_id: None,
- path: None,
- source: None,
- };
- (!reference.is_empty()).then_some(reference)
-}
-
-fn terminal_display_source(envelope: &OutputEnvelope) -> &Value {
- if !envelope.result.is_null() {
- return &envelope.result;
- }
- envelope
- .errors
- .first()
- .and_then(|error| error.detail.as_ref())
- .unwrap_or(&envelope.result)
-}
-
-fn terminal_state(result: &Value) -> Option<&str> {
- string_path(result, &["state"])
-}
-
-fn terminal_transport_profile(result: &Value) -> Option<&str> {
- string_path(result, &["publish", "mode"])
- .or_else(|| string_path(result, &["checks", "publish", "mode"]))
- .or_else(|| string_path(result, &["transport_profile"]))
-}
-
-fn terminal_publish_state(result: &Value) -> Option<&str> {
- string_path(result, &["publish", "state"])
- .or_else(|| string_path(result, &["checks", "publish", "state"]))
- .or_else(|| string_path(result, &["publish_state"]))
-}
-
-fn terminal_reason(result: &Value) -> Option<&str> {
- string_path(result, &["reason"])
- .or_else(|| string_path(result, &["publish", "reason"]))
- .or_else(|| string_path(result, &["checks", "publish", "reason"]))
- .or_else(|| string_path(result, &["store", "reason"]))
- .or_else(|| string_path(result, &["checks", "store", "reason"]))
- .or_else(|| string_path(result, &["checks", "account", "reason"]))
-}
-
-fn terminal_envelope_status(envelope: &OutputEnvelope) -> &str {
- if !envelope.errors.is_empty() {
- return "error";
- }
- if let Some(state) = envelope
- .result
- .get("state")
- .and_then(|value| value.as_str())
- {
- return state;
- }
- if envelope.dry_run {
- return "dry_run";
- }
- "ok"
-}
-
-fn terminal_status_symbol(status: &str, dry_run: bool) -> TerminalSymbol {
- if dry_run || status == "dry_run" {
- return TerminalSymbol::Neutral;
- }
- if terminal_status_needs_attention(status) {
- TerminalSymbol::Attention
- } else {
- TerminalSymbol::Success
- }
-}
-
-fn terminal_status_needs_attention(status: &str) -> bool {
- matches!(
- status,
- "needs_attention"
- | "unconfigured"
- | "unavailable"
- | "blocked"
- | "invalid"
- | "not_ready"
- | "partial"
- | "degraded"
- | "failed"
- | "conflict"
- | "missing"
- )
-}
-
-fn terminal_title(operation_id: &str, status: &str) -> String {
- format!(
- "{} {}",
- operation_title(operation_id),
- terminal_status_label(status)
- )
-}
-
-fn operation_title(operation_id: &str) -> String {
- let mut words = operation_id
- .split('.')
- .flat_map(|part| part.split('_'))
- .filter(|part| !part.is_empty())
- .map(str::to_owned)
- .collect::<Vec<_>>();
- if let Some(first) = words.first_mut() {
- *first = capitalize_ascii_word(first);
- }
- words.join(" ")
-}
-
-fn terminal_status_label(status: &str) -> String {
- status
- .split('_')
- .filter(|part| !part.is_empty())
- .collect::<Vec<_>>()
- .join(" ")
-}
-
-fn capitalize_ascii_word(word: &str) -> String {
- let mut chars = word.chars();
- let Some(first) = chars.next() else {
- return String::new();
- };
- let mut rendered = String::new();
- rendered.push(first.to_ascii_uppercase());
- rendered.extend(chars);
- rendered
-}
diff --git a/src/out/terminal/renderers/mod.rs b/src/out/terminal/renderers/mod.rs
@@ -1,2 +0,0 @@
-pub mod common;
-pub mod v1;
diff --git a/src/out/terminal/renderers/v1.rs b/src/out/terminal/renderers/v1.rs
@@ -1,25 +0,0 @@
-use crate::out::envelope::OutputEnvelope;
-use crate::out::terminal::layout::TerminalDocument;
-use crate::out::terminal::registry::{TerminalOperationRenderer, TerminalRendererRegistry};
-use crate::out::terminal::renderer::TerminalRenderContext;
-use crate::registry::OPERATION_REGISTRY;
-
-use super::common;
-
-pub fn register(registry: TerminalRendererRegistry) -> TerminalRendererRegistry {
- OPERATION_REGISTRY
- .iter()
- .fold(registry, |registry, operation| {
- registry.register(operation.operation_id, &V1_RENDERER)
- })
-}
-
-struct V1Renderer;
-
-static V1_RENDERER: V1Renderer = V1Renderer;
-
-impl TerminalOperationRenderer for V1Renderer {
- fn render(&self, envelope: &OutputEnvelope, _cx: &TerminalRenderContext) -> TerminalDocument {
- common::base_terminal_document(envelope)
- }
-}
diff --git a/src/out/terminal/tables.rs b/src/out/terminal/tables.rs
@@ -1,123 +0,0 @@
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalTableColumn {
- pub label: String,
- pub min_width: usize,
- pub max_width: usize,
-}
-
-impl TerminalTableColumn {
- pub fn new(label: impl Into<String>, min_width: usize, max_width: usize) -> Self {
- Self {
- label: label.into(),
- min_width,
- max_width: max_width.max(min_width),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalTableRow {
- pub cells: Vec<String>,
-}
-
-impl TerminalTableRow {
- pub fn new(cells: Vec<impl Into<String>>) -> Self {
- Self {
- cells: cells.into_iter().map(Into::into).collect(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TerminalTable {
- pub columns: Vec<TerminalTableColumn>,
- pub rows: Vec<TerminalTableRow>,
- pub empty: Option<String>,
-}
-
-impl TerminalTable {
- pub fn new(columns: Vec<TerminalTableColumn>) -> Self {
- Self {
- columns,
- rows: Vec::new(),
- empty: None,
- }
- }
-
- pub fn with_row(mut self, row: TerminalTableRow) -> Self {
- self.rows.push(row);
- self
- }
-
- pub fn with_empty(mut self, empty: impl Into<String>) -> Self {
- self.empty = Some(empty.into());
- self
- }
-
- pub fn column_widths(&self, width: usize) -> Vec<usize> {
- if self.columns.is_empty() {
- return Vec::new();
- }
- let separators = self.columns.len().saturating_sub(1) * 2;
- let available = width.saturating_sub(separators).max(self.columns.len());
- let mut widths = self
- .columns
- .iter()
- .map(|column| column.min_width.min(column.max_width))
- .collect::<Vec<_>>();
- let min_total = widths.iter().sum::<usize>();
- let mut remaining = available.saturating_sub(min_total);
- for (index, column) in self.columns.iter().enumerate() {
- if remaining == 0 {
- break;
- }
- let current = widths[index];
- let target = column.max_width.max(current);
- let add = target.saturating_sub(current).min(remaining);
- widths[index] += add;
- remaining -= add;
- }
- widths
- }
-}
-
-pub fn truncate_cell(value: &str, width: usize) -> String {
- if width == 0 {
- return String::new();
- }
- let chars = value.chars().collect::<Vec<_>>();
- if chars.len() <= width {
- return value.to_owned();
- }
- if width == 1 {
- return "…".to_owned();
- }
- chars
- .into_iter()
- .take(width.saturating_sub(1))
- .collect::<String>()
- + "…"
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn truncates_cells_deterministically() {
- assert_eq!(truncate_cell("abcdef", 4), "abc…");
- assert_eq!(truncate_cell("abcdef", 1), "…");
- assert_eq!(truncate_cell("abc", 4), "abc");
- }
-
- #[test]
- fn computes_stable_column_widths() {
- let table = TerminalTable::new(vec![
- TerminalTableColumn::new("Name", 4, 8),
- TerminalTableColumn::new("State", 5, 10),
- ]);
-
- assert_eq!(table.column_widths(20), vec![8, 10]);
- assert_eq!(table.column_widths(10), vec![4, 5]);
- }
-}
diff --git a/src/out/terminal/values.rs b/src/out/terminal/values.rs
@@ -1,150 +0,0 @@
-use std::path::Path;
-
-use serde_json::Value;
-
-pub fn transport_label(value: &str) -> String {
- match value {
- "nostr" => "nostr",
- "reticulum" => "reticulum",
- "local" => "local",
- other => other,
- }
- .to_owned()
-}
-
-pub fn relay_summary(success: usize, failed: usize, success_label: &str) -> String {
- format!("{success} {success_label} · {failed} failed")
-}
-
-pub fn yes_no(value: bool) -> &'static str {
- if value { "yes" } else { "no" }
-}
-
-pub fn ready_blocked(value: bool) -> &'static str {
- if value { "ready" } else { "blocked" }
-}
-
-pub fn plural(count: usize, singular: &str, plural: &str) -> String {
- if count == 1 {
- format!("1 {singular}")
- } else {
- format!("{count} {plural}")
- }
-}
-
-pub fn quantity_label(amount: impl ToString, unit: Option<&str>) -> String {
- match unit.filter(|unit| !unit.trim().is_empty()) {
- Some(unit) => format!("{} {unit}", amount.to_string()),
- None => format!("quantity {}", amount.to_string()),
- }
-}
-
-pub fn price_label(amount: impl ToString, currency: &str, unit: Option<&str>) -> String {
- match unit.filter(|unit| !unit.trim().is_empty()) {
- Some(unit) => format!("{} {currency} / {unit}", amount.to_string()),
- None => format!("{} {currency}", amount.to_string()),
- }
-}
-
-pub fn compact_id(value: &str, head: usize, tail: usize) -> String {
- let chars = value.chars().collect::<Vec<_>>();
- if chars.len() <= head + tail + 1 {
- return value.to_owned();
- }
- let start = chars.iter().take(head).collect::<String>();
- let end = chars
- .iter()
- .skip(chars.len().saturating_sub(tail))
- .collect::<String>();
- format!("{start}…{end}")
-}
-
-pub fn path_label(path: &Path) -> String {
- path.display().to_string()
-}
-
-pub fn string_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- current.as_str().filter(|value| !value.trim().is_empty())
-}
-
-pub fn bool_path(value: &Value, path: &[&str]) -> Option<bool> {
- let mut current = value;
- for segment in path {
- current = current.get(*segment)?;
- }
- current.as_bool()
-}
-
-pub fn proof_summary(value: &Value) -> Option<String> {
- if bool_path(
- value,
- &["proof_verification", "cryptographic_proof_verified"],
- ) == Some(true)
- {
- let system = string_path(value, &["proof_verification", "proof_system"])
- .or_else(|| string_path(value, &["receipt", "proof", "system"]))
- .unwrap_or("cryptographic proof");
- return Some(format!("verified · {system}"));
- }
- if string_path(value, &["proof_verification", "state"]).is_some() {
- return Some("available".to_owned());
- }
- None
-}
-
-#[cfg(test)]
-mod tests {
- use serde_json::json;
-
- use super::*;
-
- #[test]
- fn formats_domain_values() {
- assert_eq!(transport_label("nostr"), "nostr");
- assert_eq!(
- relay_summary(2, 0, "acknowledged"),
- "2 acknowledged · 0 failed"
- );
- assert_eq!(quantity_label("12", Some("lb")), "12 lb");
- assert_eq!(quantity_label("3", None), "quantity 3");
- assert_eq!(price_label("6.00", "CAD", Some("1 lb")), "6.00 CAD / 1 lb");
- assert_eq!(plural(1, "relay", "relays"), "1 relay");
- assert_eq!(plural(2, "relay", "relays"), "2 relays");
- }
-
- #[test]
- fn compacts_long_ids() {
- assert_eq!(compact_id("1234567890abcdef", 4, 3), "1234…def");
- assert_eq!(compact_id("short", 4, 3), "short");
- }
-
- #[test]
- fn proof_summary_requires_verified_true_for_verified_label() {
- assert_eq!(
- proof_summary(&json!({
- "proof_verification": {
- "cryptographic_proof_verified": true,
- "proof_system": "SP1"
- }
- }))
- .as_deref(),
- Some("verified · SP1")
- );
- assert_eq!(
- proof_summary(&json!({
- "proof_verification": {
- "state": "valid",
- "cryptographic_proof_verified": false,
- "proof_system": "SP1"
- }
- }))
- .as_deref(),
- Some("available")
- );
- assert_eq!(proof_summary(&json!({})), None);
- }
-}
diff --git a/src/registry/mod.rs b/src/registry/mod.rs
@@ -1,191 +0,0 @@
-use radroots_runtime_contract_v1::{
- ApprovalRequirementV1, DryRunSupportV1, IdempotencyPolicyV1, OperationMutabilityV1,
- RuntimeOperationDescriptorV1, RuntimeOperationIdV1, SignerRequirementV1,
- TransportCapabilityRouteV1,
-};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct OperationSpec {
- pub runtime_operation_id: RuntimeOperationIdV1,
- pub descriptor: RuntimeOperationDescriptorV1,
- pub operation_id: &'static str,
- pub cli_path: &'static str,
- pub namespace: &'static str,
- pub mcp_tool: &'static str,
- pub rust_request: &'static str,
- pub rust_result: &'static str,
- pub json_kind: &'static str,
- pub description: &'static str,
- pub role: OperationRole,
- pub supports_json: bool,
- pub supports_ndjson: bool,
-}
-
-impl OperationSpec {
- pub fn mutates(self) -> bool {
- self.descriptor.mutability == OperationMutabilityV1::Mutation
- }
-
- pub fn supports_dry_run(self) -> bool {
- self.descriptor.dry_run == DryRunSupportV1::PureLocalPlan
- }
-
- pub fn requires_idempotency(self) -> bool {
- self.descriptor.idempotency == IdempotencyPolicyV1::RequiredUuidV7
- }
-
- pub fn forbids_idempotency(self) -> bool {
- self.descriptor.idempotency == IdempotencyPolicyV1::Forbidden
- }
-
- pub fn requires_approval(self) -> bool {
- matches!(
- self.descriptor.approval,
- ApprovalRequirementV1::Required | ApprovalRequirementV1::ConditionalOrRequiredByMode
- )
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum OperationRole {
- Any,
- Buyer,
- Seller,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum NetworkRequirement {
- Local,
- External { dry_run_requires_network: bool },
-}
-
-include!("../generated/runtime_contract_registry.rs");
-
-pub fn get_operation(operation_id: &str) -> Option<&'static OperationSpec> {
- OPERATION_REGISTRY
- .iter()
- .find(|operation| operation.operation_id == operation_id)
-}
-
-pub fn runtime_operation_id(operation_id: &str) -> Option<RuntimeOperationIdV1> {
- RuntimeOperationIdV1::parse(operation_id).ok()
-}
-
-pub fn network_requirement(operation_id: &str) -> NetworkRequirement {
- let Some(spec) = get_operation(operation_id) else {
- return NetworkRequirement::Local;
- };
- if requires_external_transport(spec.descriptor.transport_capability) {
- NetworkRequirement::External {
- dry_run_requires_network: spec.descriptor.dry_run != DryRunSupportV1::PureLocalPlan,
- }
- } else {
- NetworkRequirement::Local
- }
-}
-
-pub fn requires_local_signer_mode(operation_id: &str) -> bool {
- get_operation(operation_id).is_some_and(|operation| {
- matches!(
- operation.descriptor.signer,
- SignerRequirementV1::Required | SignerRequirementV1::ConditionalRelayAuth
- )
- })
-}
-
-pub fn requires_delivery_capable_transport_profile(operation_id: &str) -> bool {
- get_operation(operation_id)
- .is_some_and(|operation| operation.descriptor.transport_capability.deliver)
-}
-
-pub fn registry_linkage_is_valid() -> bool {
- OPERATION_REGISTRY.iter().all(|operation| {
- get_operation(operation.operation_id).is_some()
- && operation.operation_id == operation.json_kind
- && operation.mcp_tool == operation.operation_id.replace('.', "_")
- && operation.supports_json
- && operation.descriptor.operation_id == operation.runtime_operation_id
- && operation.descriptor.operation_id.as_str() == operation.operation_id
- })
-}
-
-fn requires_external_transport(capability: TransportCapabilityRouteV1) -> bool {
- capability.deliver || capability.fetch || capability.synchronize || capability.diagnostics
-}
-
-#[cfg(test)]
-mod tests {
- use std::collections::BTreeSet;
-
- use radroots_runtime_contract_v1::{OperationRiskV1, RUNTIME_OPERATION_DESCRIPTORS_V1};
-
- use super::{OPERATION_REGISTRY, get_operation};
-
- #[test]
- fn registry_matches_runtime_contract_v1_catalog_exactly() {
- let actual = OPERATION_REGISTRY
- .iter()
- .map(|operation| operation.operation_id)
- .collect::<BTreeSet<_>>();
- let expected = RUNTIME_OPERATION_DESCRIPTORS_V1
- .iter()
- .map(|descriptor| descriptor.operation_id.as_str())
- .collect::<BTreeSet<_>>();
- assert_eq!(actual, expected);
- }
-
- #[test]
- fn generated_registry_identity_fields_are_consistent() {
- for operation in OPERATION_REGISTRY {
- assert_eq!(operation.operation_id, operation.json_kind);
- assert_eq!(
- operation.operation_id,
- operation.runtime_operation_id.as_str()
- );
- assert_eq!(operation.mcp_tool, operation.operation_id.replace('.', "_"));
- assert!(operation.cli_path.starts_with("radroots "));
- assert_eq!(
- operation.namespace,
- operation.operation_id.split('.').next().unwrap()
- );
- assert!(operation.supports_json);
- assert!(!operation.description.is_empty());
- assert_eq!(
- get_operation(operation.operation_id)
- .expect("operation")
- .operation_id,
- operation.operation_id
- );
- }
- }
-
- #[test]
- fn generated_registry_uses_contract_policy() {
- for operation in OPERATION_REGISTRY {
- if operation.requires_approval() {
- assert!(
- matches!(
- operation.descriptor.risk,
- OperationRiskV1::High | OperationRiskV1::Critical
- ),
- "{}",
- operation.operation_id
- );
- }
- if operation.mutates() {
- assert!(operation.supports_dry_run(), "{}", operation.operation_id);
- assert!(
- operation.requires_idempotency(),
- "{}",
- operation.operation_id
- );
- } else {
- assert!(
- operation.forbids_idempotency(),
- "{}",
- operation.operation_id
- );
- }
- }
- }
-}
diff --git a/src/runtime/account.rs b/src/runtime/account.rs
@@ -1,894 +0,0 @@
-use std::{fmt, path::Path, sync::Arc};
-
-use radroots_identity::{
- AccountId, PublicIdentity,
- account::{Record as AccountRecord, Status as AccountStatus},
-};
-use radroots_nostr_accounts::prelude::{RadrootsNostrAccountsError, RadrootsNostrAccountsManager};
-use radroots_protected_store::RadrootsProtectedFileSecretVault;
-use radroots_secret_vault::{
- RadrootsHostVaultCapabilities, RadrootsResolvedSecretBackend, RadrootsSecretBackend,
- RadrootsSecretBackendAvailability, RadrootsSecretBackendSelection, RadrootsSecretVault,
- RadrootsSecretVaultError, RadrootsSecretVaultOsKeyring,
-};
-
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::view::runtime::{AccountResolutionView, AccountSummaryView};
-
-const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_CLI_ACCOUNT_HOST_VAULT_AVAILABLE";
-const HOST_VAULT_SERVICE_NAME: &str = "org.radroots.cli.local-account";
-const HOST_VAULT_PROBE_SLOT: &str = "__radroots_cli_host_vault_probe__";
-pub const SHARED_ACCOUNT_STORE_SOURCE: &str = "shared account store · local first";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum AccountRuntimeFailure {
- Unresolved(AccountRuntimeFailureIssue),
- WatchOnly(AccountRuntimeFailureIssue),
- Mismatch(AccountRuntimeFailureIssue),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct AccountRuntimeFailureIssue {
- message: String,
- detail_json: Option<String>,
-}
-
-impl AccountRuntimeFailureIssue {
- fn new(message: impl Into<String>) -> Self {
- Self {
- message: message.into(),
- detail_json: None,
- }
- }
-
- fn with_detail(message: impl Into<String>, detail: serde_json::Value) -> Self {
- Self {
- message: message.into(),
- detail_json: Some(detail.to_string()),
- }
- }
-
- pub fn message(&self) -> &str {
- self.message.as_str()
- }
-}
-
-impl AccountRuntimeFailure {
- pub fn unresolved(message: impl Into<String>) -> Self {
- Self::Unresolved(AccountRuntimeFailureIssue::new(message))
- }
-
- pub fn unresolved_with_detail(message: impl Into<String>, detail: serde_json::Value) -> Self {
- Self::Unresolved(AccountRuntimeFailureIssue::with_detail(message, detail))
- }
-
- pub fn watch_only(account_id: &AccountId) -> Self {
- Self::WatchOnly(AccountRuntimeFailureIssue::new(format!(
- "resolved account `{account_id}` is watch_only and cannot sign because it is not secret-backed"
- )))
- }
-
- pub fn watch_only_with_detail(
- account_id: impl fmt::Display,
- detail: serde_json::Value,
- ) -> Self {
- Self::WatchOnly(AccountRuntimeFailureIssue::with_detail(
- format!(
- "resolved account `{account_id}` is watch_only and cannot sign because it is not secret-backed"
- ),
- detail,
- ))
- }
-
- pub fn mismatch(message: impl Into<String>) -> Self {
- Self::Mismatch(AccountRuntimeFailureIssue::new(message))
- }
-
- pub fn mismatch_with_detail(message: impl Into<String>, detail: serde_json::Value) -> Self {
- Self::Mismatch(AccountRuntimeFailureIssue::with_detail(message, detail))
- }
-
- pub fn message(&self) -> &str {
- match self {
- Self::Unresolved(issue) | Self::WatchOnly(issue) | Self::Mismatch(issue) => {
- issue.message.as_str()
- }
- }
- }
-
- pub fn detail_json(&self) -> Option<&str> {
- match self {
- Self::Unresolved(issue) | Self::WatchOnly(issue) | Self::Mismatch(issue) => {
- issue.detail_json.as_deref()
- }
- }
- }
-}
-
-impl fmt::Display for AccountRuntimeFailure {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(self.message())
- }
-}
-
-impl std::error::Error for AccountRuntimeFailure {}
-
-#[derive(Debug, Clone)]
-pub struct AccountSnapshot {
- pub accounts: Vec<AccountRecordView>,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountRecordView {
- pub record: AccountRecord,
- pub is_default: bool,
- pub custody: AccountCustody,
- pub write_capable: bool,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum AccountCustody {
- SecretBacked,
- WatchOnly,
-}
-
-impl AccountCustody {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::SecretBacked => "secret_backed",
- Self::WatchOnly => "watch_only",
- }
- }
-
- pub fn signer_label(self) -> &'static str {
- match self {
- Self::SecretBacked => "local",
- Self::WatchOnly => "watch_only",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct AccountRuntimeFacts {
- custody: AccountCustody,
- write_capable: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountSecretBackendStatus {
- pub state: String,
- pub active_backend: Option<String>,
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountCreateResult {
- pub account: AccountRecordView,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountClearDefaultResult {
- pub cleared_account: Option<AccountRecordView>,
- pub remaining_account_count: usize,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountRemoveResult {
- pub removed_account: AccountRecordView,
- pub default_cleared: bool,
- pub remaining_account_count: usize,
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountRemovePreview {
- pub account: AccountRecordView,
- pub default_would_clear: bool,
- pub remaining_account_count: usize,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum AccountResolutionSource {
- InvocationOverride,
- DefaultAccount,
- None,
-}
-
-impl AccountResolutionSource {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::InvocationOverride => "invocation_override",
- Self::DefaultAccount => "default_account",
- Self::None => "none",
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct AccountResolution {
- pub source: AccountResolutionSource,
- pub resolved_account: Option<AccountRecordView>,
- pub default_account: Option<AccountRecordView>,
-}
-
-#[derive(Debug)]
-pub struct AccountLocalSigner {
- pub account: AccountRecordView,
- pub signer: radroots_nostr::signing::LocalSigner,
-}
-
-pub fn create_default_account(config: &RuntimeConfig) -> Result<AccountCreateResult, RuntimeError> {
- let manager = account_manager(config)?;
- let created_account_id = manager.generate_keys(None, false)?;
-
- let snapshot = snapshot(config)?;
- let account = snapshot_account(
- &snapshot,
- &created_account_id,
- "created account missing after account create",
- )?;
-
- Ok(AccountCreateResult { account })
-}
-
-pub fn import_public_identity(
- config: &RuntimeConfig,
- path: &Path,
- make_default: bool,
-) -> Result<AccountRecordView, RuntimeError> {
- let manager = account_manager(config)?;
- let public_identity = load_public_identity_for_import(path)?;
- let imported_account_id =
- manager.upsert_public_identity(public_identity, None, make_default)?;
- let snapshot = snapshot_from_manager(&manager)?;
- snapshot_account(
- &snapshot,
- &imported_account_id,
- "imported account missing after account import",
- )
-}
-
-pub fn preview_public_identity_import(
- config: &RuntimeConfig,
- path: &Path,
- make_default: bool,
-) -> Result<AccountRecordView, RuntimeError> {
- let public_identity = load_public_identity_for_import(path)?;
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- if let Some(existing) = snapshot
- .accounts
- .iter()
- .find(|account| account.record.id() == AccountId::from(&public_identity))
- .cloned()
- {
- let mut account = existing;
- if make_default {
- account.is_default = true;
- }
- return Ok(account);
- }
-
- Ok(AccountRecordView {
- record: AccountRecord::new(public_identity, None, 0),
- is_default: make_default,
- custody: AccountCustody::WatchOnly,
- write_capable: false,
- })
-}
-
-pub fn preview_identity_secret_attachment(
- config: &RuntimeConfig,
- selector: &str,
- path: &Path,
- make_default: bool,
-) -> Result<AccountRecordView, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let mut account = resolve_selector_account(&manager, &snapshot, selector)?;
- let secret = load_secret_key_for_attachment(path)?;
- validate_secret_matches_account(&account.record, &secret)?;
- if make_default {
- account.is_default = true;
- }
- account.custody = AccountCustody::SecretBacked;
- account.write_capable = true;
- Ok(account)
-}
-
-pub fn attach_identity_secret(
- config: &RuntimeConfig,
- selector: &str,
- path: &Path,
- make_default: bool,
-) -> Result<AccountRecordView, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let account = resolve_selector_account(&manager, &snapshot, selector)?;
- let secret = load_secret_key_for_attachment(path)?;
- validate_secret_matches_account(&account.record, &secret)?;
- let keys = nostr::Keys::new(secret);
- let attached = manager.attach_secret_keys(&account.record.id(), &keys, make_default)?;
- let snapshot = snapshot_from_manager(&manager)?;
- snapshot_account(
- &snapshot,
- &attached.id(),
- "attached account missing after account secret attachment",
- )
-}
-
-pub fn snapshot(config: &RuntimeConfig) -> Result<AccountSnapshot, RuntimeError> {
- let manager = account_manager(config)?;
- snapshot_from_manager(&manager)
-}
-
-pub fn resolve_account(config: &RuntimeConfig) -> Result<Option<AccountRecordView>, RuntimeError> {
- Ok(resolve_account_resolution(config)?.resolved_account)
-}
-
-pub fn resolve_account_resolution(
- config: &RuntimeConfig,
-) -> Result<AccountResolution, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let default_account = snapshot
- .accounts
- .iter()
- .find(|account| account.is_default)
- .cloned();
- if let Some(selector) = config.account.selector.as_deref() {
- let account = resolve_selector_account(&manager, &snapshot, selector)?;
- return Ok(AccountResolution {
- source: AccountResolutionSource::InvocationOverride,
- resolved_account: Some(account),
- default_account,
- });
- }
-
- Ok(AccountResolution {
- source: if default_account.is_some() {
- AccountResolutionSource::DefaultAccount
- } else {
- AccountResolutionSource::None
- },
- resolved_account: default_account.clone(),
- default_account,
- })
-}
-
-pub fn select_account(
- config: &RuntimeConfig,
- selector: &str,
-) -> Result<AccountRecordView, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let account = resolve_selector_account(&manager, &snapshot, selector)?;
-
- manager.set_default_account(&account.record.id())?;
- let snapshot = snapshot_from_manager(&manager)?;
- snapshot
- .accounts
- .into_iter()
- .find(|candidate| candidate.record.id() == account.record.id())
- .ok_or_else(|| {
- RuntimeError::Accounts(
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsError::InvalidState(
- "default account missing after account use".to_owned(),
- ),
- )
- })
-}
-
-pub fn resolve_account_selector(
- config: &RuntimeConfig,
- selector: &str,
-) -> Result<AccountRecordView, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- resolve_selector_account(&manager, &snapshot, selector)
-}
-
-pub fn clear_default_account(
- config: &RuntimeConfig,
-) -> Result<AccountClearDefaultResult, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let cleared_account = snapshot
- .accounts
- .iter()
- .find(|account| account.is_default)
- .cloned();
- manager.clear_default_account()?;
- let remaining_account_count = snapshot_from_manager(&manager)?.accounts.len();
- Ok(AccountClearDefaultResult {
- cleared_account,
- remaining_account_count,
- })
-}
-
-pub fn remove_account(
- config: &RuntimeConfig,
- selector: &str,
-) -> Result<AccountRemoveResult, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let removed_account = resolve_selector_account(&manager, &snapshot, selector)?;
- let default_cleared = removed_account.is_default;
- manager.remove_account(&removed_account.record.id())?;
- let remaining_account_count = snapshot_from_manager(&manager)?.accounts.len();
- Ok(AccountRemoveResult {
- removed_account,
- default_cleared,
- remaining_account_count,
- })
-}
-
-pub fn preview_account_removal(
- config: &RuntimeConfig,
- selector: &str,
-) -> Result<AccountRemovePreview, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let account = resolve_selector_account(&manager, &snapshot, selector)?;
- Ok(AccountRemovePreview {
- default_would_clear: account.is_default,
- remaining_account_count: snapshot.accounts.len().saturating_sub(1),
- account,
- })
-}
-
-pub fn resolved_account_signing_status(
- config: &RuntimeConfig,
-) -> Result<AccountStatus, RuntimeError> {
- let manager = account_manager(config)?;
- let resolution = resolve_account_resolution(config)?;
- let Some(account) = resolution.resolved_account else {
- return Ok(AccountStatus::NotConfigured);
- };
-
- Ok(match manager.get_signing_keys(&account.record.id())? {
- Some(_) => AccountStatus::Ready {
- account: account.record.clone(),
- },
- None => AccountStatus::PublicOnly {
- account: account.record.clone(),
- },
- })
-}
-
-pub fn resolve_local_signing_identity(
- config: &RuntimeConfig,
-) -> Result<AccountLocalSigner, RuntimeError> {
- let manager = account_manager(config)?;
- let resolution = resolve_account_resolution(config)?;
- let Some(account) = resolution.resolved_account else {
- return Err(AccountRuntimeFailure::unresolved(unresolved_account_reason(config)?).into());
- };
- let Some(secret) = manager.export_secret_hex(&account.record.id())? else {
- return Err(AccountRuntimeFailure::watch_only(&account.record.id()).into());
- };
- let secret = zeroize::Zeroizing::new(secret);
- let secret = radroots_nostr::key::parse_secret_key(secret.trim())
- .map_err(|_| RuntimeError::Config("local account secret is invalid".to_owned()))?;
- let signer = radroots_nostr::signing::LocalSigner::new(secret)
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
- Ok(AccountLocalSigner { account, signer })
-}
-
-pub fn resolve_local_signing_identity_for_account(
- config: &RuntimeConfig,
- account_id: &str,
-) -> Result<AccountLocalSigner, RuntimeError> {
- let manager = account_manager(config)?;
- let snapshot = snapshot_from_manager(&manager)?;
- let Some(account) = snapshot
- .accounts
- .iter()
- .find(|account| account.record.id().to_hex() == account_id)
- .cloned()
- else {
- return Err(AccountRuntimeFailure::unresolved(format!(
- "farm-bound seller account `{account_id}` is not present in the local account store"
- ))
- .into());
- };
- let Some(secret) = manager.export_secret_hex(&account.record.id())? else {
- return Err(AccountRuntimeFailure::watch_only(&account.record.id()).into());
- };
- let secret = zeroize::Zeroizing::new(secret);
- let secret = radroots_nostr::key::parse_secret_key(secret.trim())
- .map_err(|_| RuntimeError::Config("local account secret is invalid".to_owned()))?;
- let signer = radroots_nostr::signing::LocalSigner::new(secret)
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
- Ok(AccountLocalSigner { account, signer })
-}
-
-pub fn account_summary_view(account: &AccountRecordView) -> AccountSummaryView {
- AccountSummaryView::from_account_runtime(
- &account.record,
- account.custody.signer_label(),
- account.custody.as_str(),
- account.write_capable,
- account.is_default,
- )
-}
-
-pub fn account_resolution_view(resolution: &AccountResolution) -> AccountResolutionView {
- AccountResolutionView {
- status: if resolution.resolved_account.is_some() {
- "resolved"
- } else {
- "unresolved"
- }
- .to_owned(),
- source: resolution.source.as_str().to_owned(),
- resolved_account: resolution
- .resolved_account
- .as_ref()
- .map(account_summary_view),
- default_account: resolution
- .default_account
- .as_ref()
- .map(account_summary_view),
- }
-}
-
-pub fn empty_account_resolution_view() -> AccountResolutionView {
- AccountResolutionView {
- status: "unresolved".to_owned(),
- source: AccountResolutionSource::None.as_str().to_owned(),
- resolved_account: None,
- default_account: None,
- }
-}
-
-pub fn unresolved_account_reason(config: &RuntimeConfig) -> Result<String, RuntimeError> {
- let snapshot = snapshot(config)?;
- Ok(if snapshot.accounts.is_empty() {
- format!(
- "no local accounts found in {}",
- config.account.store_path.display()
- )
- } else {
- format!(
- "accounts exist in {} but no default account is configured and no invocation override was provided",
- config.account.store_path.display()
- )
- })
-}
-
-pub fn secret_backend_status(config: &RuntimeConfig) -> AccountSecretBackendStatus {
- match resolve_secret_backend(config) {
- Ok(resolved) => AccountSecretBackendStatus {
- state: "ready".to_owned(),
- active_backend: Some(resolved.backend.kind().to_string()),
- reason: None,
- },
- Err(SecretBackendResolutionError::Unavailable(reason)) => AccountSecretBackendStatus {
- state: "unavailable".to_owned(),
- active_backend: None,
- reason: Some(reason),
- },
- Err(SecretBackendResolutionError::Invalid(reason)) => AccountSecretBackendStatus {
- state: "error".to_owned(),
- active_backend: None,
- reason: Some(reason),
- },
- }
-}
-
-pub fn load_secret_backend_secret(
- config: &RuntimeConfig,
- slot: &str,
- service_name: &str,
-) -> Result<Option<String>, RuntimeError> {
- if slot.trim().is_empty() {
- return Err(RuntimeError::Config(
- "secret backend slot must not be empty".to_owned(),
- ));
- }
- let resolved = resolve_secret_backend(config).map_err(secret_backend_resolution_error)?;
- let vault = secret_vault_for_backend(config, resolved.backend, service_name)?;
- vault.load_secret(slot).map_err(|error| {
- RuntimeError::Config(format!(
- "failed to load secret `{slot}` from account secret backend `{}`: {error}",
- resolved.backend.kind()
- ))
- })
-}
-
-fn snapshot_from_manager(
- manager: &RadrootsNostrAccountsManager,
-) -> Result<AccountSnapshot, RuntimeError> {
- let default_account_id = manager.default_account_id()?.map(|id| id.to_string());
- let mut accounts = Vec::new();
- for record in manager.list_accounts()? {
- let is_default = default_account_id
- .as_deref()
- .is_some_and(|default| default == record.id().to_hex());
- let runtime = account_runtime_facts(manager, &record)?;
- accounts.push(AccountRecordView {
- record,
- is_default,
- custody: runtime.custody,
- write_capable: runtime.write_capable,
- });
- }
-
- Ok(AccountSnapshot { accounts })
-}
-
-fn snapshot_account(
- snapshot: &AccountSnapshot,
- account_id: &AccountId,
- missing_message: &str,
-) -> Result<AccountRecordView, RuntimeError> {
- snapshot
- .accounts
- .iter()
- .find(|account| account.record.id() == *account_id)
- .cloned()
- .ok_or_else(|| {
- RuntimeError::Accounts(
- radroots_nostr_accounts::prelude::RadrootsNostrAccountsError::InvalidState(
- missing_message.to_owned(),
- ),
- )
- })
-}
-
-fn resolve_selector_account(
- manager: &RadrootsNostrAccountsManager,
- snapshot: &AccountSnapshot,
- selector: &str,
-) -> Result<AccountRecordView, RuntimeError> {
- let record = manager
- .resolve_account_selector(selector)
- .map_err(|error| selector_runtime_error(selector, error))?;
- snapshot
- .accounts
- .iter()
- .find(|account| account.record.id() == record.id())
- .cloned()
- .ok_or_else(|| {
- RuntimeError::Accounts(RadrootsNostrAccountsError::InvalidState(
- "resolved account missing from snapshot".to_owned(),
- ))
- })
-}
-
-fn selector_runtime_error(selector: &str, error: RadrootsNostrAccountsError) -> RuntimeError {
- let normalized = selector.trim();
- match error {
- RadrootsNostrAccountsError::InvalidAccountSelector(reason) => RuntimeError::Config(reason),
- RadrootsNostrAccountsError::AccountNotFound(_) => {
- AccountRuntimeFailure::unresolved(format!(
- "account selector `{normalized}` did not match any local account"
- ))
- .into()
- }
- RadrootsNostrAccountsError::AmbiguousAccountSelector(_) => {
- AccountRuntimeFailure::unresolved(format!(
- "account selector `{normalized}` matched multiple local accounts; use account id or npub"
- ))
- .into()
- }
- other => RuntimeError::Accounts(other),
- }
-}
-
-fn account_runtime_facts(
- manager: &RadrootsNostrAccountsManager,
- record: &AccountRecord,
-) -> Result<AccountRuntimeFacts, RuntimeError> {
- Ok(if manager.get_signing_keys(&record.id())?.is_some() {
- AccountRuntimeFacts {
- custody: AccountCustody::SecretBacked,
- write_capable: true,
- }
- } else {
- AccountRuntimeFacts {
- custody: AccountCustody::WatchOnly,
- write_capable: false,
- }
- })
-}
-
-fn load_public_identity_for_import(path: &Path) -> Result<PublicIdentity, RuntimeError> {
- let bytes = std::fs::read(path).map_err(|error| {
- RuntimeError::Config(format!(
- "failed to read account import {}: {error}",
- path.display()
- ))
- })?;
- serde_json::from_slice(&bytes).map_err(|error| {
- RuntimeError::Config(format!(
- "failed to import canonical public identity from {}: {error}",
- path.display()
- ))
- })
-}
-
-fn load_secret_key_for_attachment(path: &Path) -> Result<nostr::SecretKey, RuntimeError> {
- let secret = std::fs::read_to_string(path).map_err(|error| {
- RuntimeError::Config(format!(
- "failed to read account secret from {}: {error}",
- path.display(),
- ))
- })?;
- nostr::SecretKey::parse(secret.trim()).map_err(|_| {
- RuntimeError::Config(format!(
- "failed to import account secret from {}: invalid hex or nsec key",
- path.display()
- ))
- })
-}
-
-fn validate_secret_matches_account(
- record: &AccountRecord,
- secret: &nostr::SecretKey,
-) -> Result<(), RuntimeError> {
- let secret_public_key_hex = nostr::Keys::new(secret.clone()).public_key().to_hex();
- let public_key_hex = record.public_identity().public_key().to_hex();
- if public_key_hex.eq_ignore_ascii_case(secret_public_key_hex.as_str()) {
- return Ok(());
- }
-
- Err(AccountRuntimeFailure::mismatch(format!(
- "account mismatch: resolved account `{}` public key `{}` does not match secret public key `{}`",
- record.id(), public_key_hex, secret_public_key_hex
- ))
- .into())
-}
-
-fn account_manager(config: &RuntimeConfig) -> Result<RadrootsNostrAccountsManager, RuntimeError> {
- let (manager, _) = RadrootsNostrAccountsManager::new_local_file_backed(
- config.account.store_path.as_path(),
- config.account.secrets_dir.as_path(),
- account_secret_backend_selection(config),
- secret_backend_availability()?,
- HOST_VAULT_SERVICE_NAME,
- )?;
- Ok(manager)
-}
-
-fn resolve_secret_backend(
- config: &RuntimeConfig,
-) -> Result<RadrootsResolvedSecretBackend, SecretBackendResolutionError> {
- let availability = secret_backend_availability().map_err(|error| {
- SecretBackendResolutionError::Invalid(format!("account secret backend: {error}"))
- })?;
- RadrootsNostrAccountsManager::resolve_local_backend(
- account_secret_backend_selection(config),
- availability,
- )
- .map_err(|error| match error {
- RadrootsSecretVaultError::BackendUnavailable { .. } => {
- SecretBackendResolutionError::Unavailable(format!("account secret backend: {error}"))
- }
- RadrootsSecretVaultError::HostVaultPolicyUnsupported { .. } => {
- SecretBackendResolutionError::Invalid(format!("account secret backend: {error}"))
- }
- })
-}
-
-fn secret_backend_resolution_error(error: SecretBackendResolutionError) -> RuntimeError {
- match error {
- SecretBackendResolutionError::Unavailable(reason)
- | SecretBackendResolutionError::Invalid(reason) => RuntimeError::Config(reason),
- }
-}
-
-pub(crate) fn account_secret_vault(
- config: &RuntimeConfig,
-) -> Result<Arc<dyn RadrootsSecretVault>, RuntimeError> {
- let resolved = resolve_secret_backend(config).map_err(secret_backend_resolution_error)?;
- secret_vault_for_backend(config, resolved.backend, HOST_VAULT_SERVICE_NAME)
-}
-
-fn secret_vault_for_backend(
- config: &RuntimeConfig,
- backend: RadrootsSecretBackend,
- service_name: &str,
-) -> Result<Arc<dyn RadrootsSecretVault>, RuntimeError> {
- match backend {
- RadrootsSecretBackend::HostVault(_) => {
- Ok(Arc::new(RadrootsSecretVaultOsKeyring::new(service_name)))
- }
- RadrootsSecretBackend::EncryptedFile => Ok(Arc::new(
- RadrootsProtectedFileSecretVault::new(config.account.secrets_dir.as_path()),
- )),
- RadrootsSecretBackend::ExternalCommand => Err(RuntimeError::Config(
- "external_command account secret backend is not supported for CLI signer sessions"
- .to_owned(),
- )),
- RadrootsSecretBackend::Memory => Err(RuntimeError::Config(
- "memory account secret backend is not supported for persisted CLI signer sessions"
- .to_owned(),
- )),
- }
-}
-
-fn account_secret_backend_selection(config: &RuntimeConfig) -> RadrootsSecretBackendSelection {
- RadrootsSecretBackendSelection {
- primary: config.account.secret_backend,
- }
-}
-
-fn secret_backend_availability() -> Result<RadrootsSecretBackendAvailability, RuntimeError> {
- Ok(RadrootsSecretBackendAvailability {
- host_vault: host_vault_capabilities()?,
- encrypted_file: true,
- external_command: false,
- memory: true,
- })
-}
-
-fn host_vault_capabilities() -> Result<RadrootsHostVaultCapabilities, RuntimeError> {
- if let Some(available) = host_vault_availability_override()? {
- return Ok(match available {
- true => RadrootsHostVaultCapabilities::desktop_keyring(),
- false => RadrootsHostVaultCapabilities::unavailable(),
- });
- }
-
- let keyring = RadrootsSecretVaultOsKeyring::new(HOST_VAULT_SERVICE_NAME);
- match keyring.load_secret(HOST_VAULT_PROBE_SLOT) {
- Ok(_) => Ok(RadrootsHostVaultCapabilities::desktop_keyring()),
- Err(_) => Ok(RadrootsHostVaultCapabilities::unavailable()),
- }
-}
-
-fn host_vault_availability_override() -> Result<Option<bool>, RuntimeError> {
- let Ok(value) = std::env::var(HOST_VAULT_AVAILABILITY_OVERRIDE_ENV) else {
- return Ok(None);
- };
-
- parse_bool_value(HOST_VAULT_AVAILABILITY_OVERRIDE_ENV, value.trim()).map(Some)
-}
-
-fn parse_bool_value(key: &str, value: &str) -> Result<bool, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "1" | "true" | "yes" | "on" => Ok(true),
- "0" | "false" | "no" | "off" => Ok(false),
- other => Err(RuntimeError::Config(format!(
- "{key} must be a boolean value, got `{other}`"
- ))),
- }
-}
-
-#[derive(Debug, Clone)]
-enum SecretBackendResolutionError {
- Unavailable(String),
- Invalid(String),
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_protected_store::RadrootsProtectedFileSecretVault;
- use radroots_secret_vault::RadrootsSecretVault;
- use std::fs;
- use tempfile::tempdir;
-
- #[test]
- fn protected_file_vault_round_trips_secret() {
- let temp = tempdir().expect("tempdir");
- let vault = RadrootsProtectedFileSecretVault::new(temp.path());
-
- vault.store_secret("acct_demo", "deadbeef").expect("store");
- let loaded = vault.load_secret("acct_demo").expect("load");
- assert_eq!(loaded.as_deref(), Some("deadbeef"));
- let raw = fs::read_to_string(temp.path().join("acct_demo.secret.json")).expect("raw file");
- assert!(!raw.contains("deadbeef"));
- }
-
- #[test]
- fn protected_file_vault_removes_secret() {
- let temp = tempdir().expect("tempdir");
- let vault = RadrootsProtectedFileSecretVault::new(temp.path());
-
- vault.store_secret("acct_demo", "deadbeef").expect("store");
- vault.remove_secret("acct_demo").expect("remove");
- assert!(vault.load_secret("acct_demo").expect("load").is_none());
- }
-}
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -1,4111 +0,0 @@
-use std::collections::BTreeMap;
-use std::fs;
-use std::io::IsTerminal;
-use std::path::Path;
-use std::path::PathBuf;
-
-use radroots_runtime::{parse_bool_value, parse_strict_env_file, parse_u64_value};
-use radroots_runtime_paths::RadrootsPathResolver;
-use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
-use radroots_transport::RadrootsTransportMeshScopeId;
-use radroots_transport_nostr::{Error as RelayTransportError, RelayUrl, RelayUrlPolicy};
-use serde::Deserialize;
-use url::Url;
-
-use crate::cli::global::RuntimeInvocationArgs;
-use crate::runtime::RuntimeError;
-pub use crate::runtime::paths::PathsConfig;
-use crate::runtime::paths::{ENV_CLI_PATHS_PROFILE, ENV_CLI_PATHS_REPO_LOCAL_ROOT, resolve_paths};
-
-const DEFAULT_LOG_FILTER: &str = "info";
-pub(crate) const RADROOTS_RETICULUM_SCOPE_ID: &str = "local";
-pub(crate) const RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE: &str =
- "Reticulum transport is not available in this release";
-const DEFAULT_ENV_PATH: &str = ".env";
-const DEFAULT_LOCAL_STATE_DIR: &str = "replica";
-const DEFAULT_LOCAL_DB_FILE: &str = "replica.sqlite";
-const DEFAULT_LOCAL_BACKUPS_DIR: &str = "backups";
-const DEFAULT_LOCAL_EXPORTS_DIR: &str = "exports";
-const DEFAULT_SHARED_ACCOUNTS_STORE_FILE: &str = "store.json";
-const DEFAULT_MYC_STATUS_TIMEOUT_MS: u64 = 2_000;
-const DEFAULT_HYF_EXECUTABLE: &str = "hyfd";
-const DEFAULT_RPC_URL: &str = "http://127.0.0.1:7070";
-const CLI_HOST_VAULT_POLICY: &str = "desktop";
-const CLI_DEFAULT_SECRET_BACKEND: &str = "host_vault";
-const CLI_ALLOWED_SHARED_SECRET_BACKENDS: &[&str] = &["host_vault", "encrypted_file"];
-const CLI_USES_PROTECTED_STORE: bool = true;
-const ENV_CLI_FILE_PATH: &str = "RADROOTS_CLI_ENV_FILE";
-const ENV_CLI_OUTPUT_FORMAT: &str = "RADROOTS_CLI_OUTPUT_FORMAT";
-const ENV_CLI_LOG_FILTER: &str = "RADROOTS_CLI_LOGGING_FILTER";
-const ENV_CLI_LOG_DIR: &str = "RADROOTS_CLI_LOGGING_OUTPUT_DIR";
-const ENV_CLI_LOG_STDOUT: &str = "RADROOTS_CLI_LOGGING_STDOUT";
-const ENV_CLI_ACCOUNT_SELECTOR: &str = "RADROOTS_CLI_ACCOUNT_SELECTOR";
-const ENV_CLI_ACCOUNT_SECRET_BACKEND: &str = "RADROOTS_CLI_ACCOUNT_SECRET_BACKEND";
-const ENV_CLI_IDENTITY_PATH: &str = "RADROOTS_CLI_IDENTITY_PATH";
-const ENV_CLI_SIGNER_BACKEND: &str = "RADROOTS_CLI_SIGNER_BACKEND";
-const ENV_CLI_TRANSPORT_PROFILE: &str = "RADROOTS_CLI_TRANSPORT_PROFILE";
-const ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS: &str = "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS";
-const ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR: &str = "RADROOTS_CLI_TRANSPORT_RETICULUM_BEHAVIOR";
-const ENV_CLI_TRANSPORT_RETICULUM_SCOPE: &str = "RADROOTS_CLI_TRANSPORT_RETICULUM_SCOPE";
-const ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT: &str =
- "RADROOTS_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT";
-const ENV_CLI_RADROOTSD_EXECUTION_URL: &str = "RADROOTS_CLI_RADROOTSD_EXECUTION_URL";
-const ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE: &str = "RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_FILE";
-const ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID: &str =
- "RADROOTS_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID";
-const ENV_CLI_MYC_EXECUTABLE: &str = "RADROOTS_CLI_MYC_EXECUTABLE";
-const ENV_CLI_MYC_STATUS_TIMEOUT_MS: &str = "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS";
-const ENV_CLI_HYF_ENABLED: &str = "RADROOTS_CLI_HYF_ENABLED";
-const ENV_CLI_HYF_EXECUTABLE: &str = "RADROOTS_CLI_HYF_EXECUTABLE";
-const ENV_CLI_RHI_VALIDATOR_SET_ID: &str = "RADROOTS_CLI_RHI_VALIDATOR_SET_ID";
-const ENV_CLI_RHI_VALIDATOR_PUBKEY: &str = "RADROOTS_CLI_RHI_VALIDATOR_PUBKEY";
-const ENV_CLI_RHI_VALIDATOR_SET_ADDR: &str = "RADROOTS_CLI_RHI_VALIDATOR_SET_ADDR";
-const ENV_CLI_RHI_VALIDATOR_SET_EVENT_ID: &str = "RADROOTS_CLI_RHI_VALIDATOR_SET_EVENT_ID";
-const ENV_CLI_RHI_VALIDATOR_SET_VALID_FROM: &str = "RADROOTS_CLI_RHI_VALIDATOR_SET_VALID_FROM";
-const ENV_CLI_RHI_VALIDATOR_SET_VALID_UNTIL: &str = "RADROOTS_CLI_RHI_VALIDATOR_SET_VALID_UNTIL";
-const ENV_CLI_RHI_VALIDATOR_SET_PROTOCOL_CONTRACT_HASH: &str =
- "RADROOTS_CLI_RHI_VALIDATOR_SET_PROTOCOL_CONTRACT_HASH";
-const ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_NAME: &str =
- "RADROOTS_CLI_RHI_VALIDATOR_SET_OPERATOR_NAME";
-const ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_CONTACT: &str =
- "RADROOTS_CLI_RHI_VALIDATOR_SET_OPERATOR_CONTACT";
-const ENV_CLI_RHI_REQUIRE_CRYPTOGRAPHIC_PROOF: &str =
- "RADROOTS_CLI_RHI_REQUIRE_CRYPTOGRAPHIC_PROOF";
-const ENV_CLI_MESH_SCOPE: &str = "RADROOTS_CLI_MESH_SCOPE";
-const SUPPORTED_ENV_FILE_KEYS: &[&str] = &[
- ENV_CLI_OUTPUT_FORMAT,
- ENV_CLI_LOG_FILTER,
- ENV_CLI_LOG_DIR,
- ENV_CLI_LOG_STDOUT,
- ENV_CLI_PATHS_PROFILE,
- ENV_CLI_PATHS_REPO_LOCAL_ROOT,
- ENV_CLI_ACCOUNT_SELECTOR,
- ENV_CLI_ACCOUNT_SECRET_BACKEND,
- ENV_CLI_IDENTITY_PATH,
- ENV_CLI_SIGNER_BACKEND,
- ENV_CLI_TRANSPORT_PROFILE,
- ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS,
- ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR,
- ENV_CLI_TRANSPORT_RETICULUM_SCOPE,
- ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT,
- ENV_CLI_RADROOTSD_EXECUTION_URL,
- ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE,
- ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID,
- ENV_CLI_MYC_EXECUTABLE,
- ENV_CLI_MYC_STATUS_TIMEOUT_MS,
- ENV_CLI_HYF_ENABLED,
- ENV_CLI_HYF_EXECUTABLE,
- ENV_CLI_RHI_VALIDATOR_SET_ID,
- ENV_CLI_RHI_VALIDATOR_PUBKEY,
- ENV_CLI_RHI_VALIDATOR_SET_ADDR,
- ENV_CLI_RHI_VALIDATOR_SET_EVENT_ID,
- ENV_CLI_RHI_VALIDATOR_SET_VALID_FROM,
- ENV_CLI_RHI_VALIDATOR_SET_VALID_UNTIL,
- ENV_CLI_RHI_VALIDATOR_SET_PROTOCOL_CONTRACT_HASH,
- ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_NAME,
- ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_CONTACT,
- ENV_CLI_RHI_REQUIRE_CRYPTOGRAPHIC_PROOF,
- ENV_CLI_MESH_SCOPE,
-];
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum OutputFormat {
- Terminal,
- Json,
- Ndjson,
-}
-
-impl OutputFormat {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Terminal => "terminal",
- Self::Json => "json",
- Self::Ndjson => "ndjson",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Verbosity {
- Quiet,
- Normal,
- Verbose,
- Trace,
-}
-
-impl Verbosity {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Quiet => "quiet",
- Self::Normal => "normal",
- Self::Verbose => "verbose",
- Self::Trace => "trace",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct OutputConfig {
- pub format: OutputFormat,
- pub verbosity: Verbosity,
- pub dry_run: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct InteractionConfig {
- pub input_enabled: bool,
- pub assume_yes: bool,
- pub stdin_tty: bool,
- pub stdout_tty: bool,
- pub prompts_allowed: bool,
- pub confirmations_allowed: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct LoggingConfig {
- pub filter: String,
- pub directory: Option<PathBuf>,
- pub stdout: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct IdentityConfig {
- pub path: PathBuf,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct AccountConfig {
- pub selector: Option<String>,
- pub store_path: PathBuf,
- pub secrets_dir: PathBuf,
- pub secret_backend: RadrootsSecretBackend,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct AccountSecretContractConfig {
- pub default_backend: String,
- pub allowed_backends: Vec<String>,
- pub host_vault_policy: Option<String>,
- pub uses_protected_store: bool,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum SignerBackend {
- Local,
- Myc,
-}
-
-impl SignerBackend {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Local => "local",
- Self::Myc => "myc",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SignerConfig {
- pub backend: SignerBackend,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TransportProfileKind {
- LocalOnly,
- Nostr,
- Reticulum,
- MultiTarget,
-}
-
-impl TransportProfileKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::LocalOnly => "local_only",
- Self::Nostr => "nostr",
- Self::Reticulum => "reticulum",
- Self::MultiTarget => "multi_target",
- }
- }
-
- pub fn transport_family(self) -> &'static str {
- match self {
- Self::LocalOnly => "local",
- Self::Nostr => "nostr",
- Self::Reticulum => "reticulum",
- Self::MultiTarget => "multi_target",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ReticulumBehavior {
- RejectDeliveryAttempts,
- DeferDeliveryPlans,
-}
-
-impl ReticulumBehavior {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::RejectDeliveryAttempts => "reject_delivery_attempts",
- Self::DeferDeliveryPlans => "defer_delivery_plans",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum TransportConfigSource {
- Environment,
- UserConfig,
- WorkspaceConfig,
- Defaults,
-}
-
-impl TransportConfigSource {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Environment => "environment · transport profile",
- Self::UserConfig => "user config · transport profile",
- Self::WorkspaceConfig => "workspace config · transport profile",
- Self::Defaults => "defaults · transport profile",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TransportConfig {
- pub profile: TransportProfileKind,
- pub source: TransportConfigSource,
- pub nostr_relay_urls: Vec<String>,
- pub reticulum_behavior: ReticulumBehavior,
- pub reticulum_scope: String,
- pub reticulum_agent_endpoint: Option<String>,
- pub radrootsd_execution: RadrootsdExecutionConfig,
-}
-
-impl TransportConfig {
- pub fn local_only() -> Self {
- Self {
- profile: TransportProfileKind::LocalOnly,
- source: TransportConfigSource::Defaults,
- nostr_relay_urls: Vec::new(),
- reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts,
- reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(),
- reticulum_agent_endpoint: None,
- radrootsd_execution: RadrootsdExecutionConfig::default(),
- }
- }
-
- pub fn from_nostr_relay_urls(nostr_relay_urls: Vec<String>) -> Self {
- if nostr_relay_urls.is_empty() {
- return Self::local_only();
- }
- Self {
- profile: TransportProfileKind::Nostr,
- source: TransportConfigSource::Defaults,
- nostr_relay_urls,
- reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts,
- reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(),
- reticulum_agent_endpoint: None,
- radrootsd_execution: RadrootsdExecutionConfig::default(),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RadrootsdExecutionConfig {
- pub url: String,
- pub token_file: Option<PathBuf>,
- pub token_secret_id: Option<String>,
-}
-
-impl Default for RadrootsdExecutionConfig {
- fn default() -> Self {
- Self {
- url: DEFAULT_RPC_URL.to_owned(),
- token_file: None,
- token_secret_id: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct LocalConfig {
- pub root: PathBuf,
- pub replica_store_path: PathBuf,
- pub backups_dir: PathBuf,
- pub exports_dir: PathBuf,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MycConfig {
- pub executable: PathBuf,
- pub status_timeout_ms: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HyfConfig {
- pub enabled: bool,
- pub executable: PathBuf,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum MeshScope {
- Disabled,
- Local,
-}
-
-impl MeshScope {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Disabled => "disabled",
- Self::Local => "local",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MeshConfig {
- pub scope: MeshScope,
- pub source: MeshConfigSource,
-}
-
-impl MeshConfig {
- pub fn disabled() -> Self {
- Self {
- scope: MeshScope::Disabled,
- source: MeshConfigSource::Defaults,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum MeshConfigSource {
- Environment,
- UserConfig,
- WorkspaceConfig,
- Defaults,
-}
-
-impl MeshConfigSource {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Environment => "environment · local first",
- Self::UserConfig => "user config · local first",
- Self::WorkspaceConfig => "workspace config · local first",
- Self::Defaults => "defaults · local first",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CapabilityBindingTargetKind {
- ManagedInstance,
- ExplicitEndpoint,
-}
-
-impl CapabilityBindingTargetKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::ManagedInstance => "managed_instance",
- Self::ExplicitEndpoint => "explicit_endpoint",
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CapabilityBindingSource {
- UserConfig,
- WorkspaceConfig,
-}
-
-impl CapabilityBindingSource {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::UserConfig => "user config [[capability_binding]]",
- Self::WorkspaceConfig => "workspace config [[capability_binding]]",
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CapabilityBindingConfig {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub target_kind: CapabilityBindingTargetKind,
- pub target: String,
- pub managed_account_ref: Option<String>,
- pub signer_session_ref: Option<String>,
- pub source: CapabilityBindingSource,
-}
-
-#[cfg(test)]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CapabilityBindingInspectionState {
- Configured,
- NotConfigured,
- Disabled,
-}
-
-#[cfg(test)]
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CapabilityBindingInspection {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: CapabilityBindingInspectionState,
- pub source: String,
- pub target_kind: Option<String>,
- pub target: Option<String>,
- pub managed_account_ref: Option<String>,
- pub signer_session_ref: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RpcConfig {
- pub url: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RhiConfig {
- pub validator_set: Option<RhiValidatorSetConfig>,
- pub require_cryptographic_proof: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RhiValidatorSetConfig {
- pub set_id: String,
- pub validator_pubkey: String,
- pub validator_set_addr: String,
- pub validator_set_event_id: String,
- pub valid_from: u64,
- pub valid_until: u64,
- pub protocol_contract_hash: String,
- pub operator_name: String,
- pub operator_contact: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RuntimeConfig {
- pub output: OutputConfig,
- pub interaction: InteractionConfig,
- pub paths: PathsConfig,
- pub logging: LoggingConfig,
- pub account: AccountConfig,
- pub account_secret_contract: AccountSecretContractConfig,
- pub identity: IdentityConfig,
- pub signer: SignerConfig,
- pub transport: TransportConfig,
- pub local: LocalConfig,
- pub myc: MycConfig,
- pub hyf: HyfConfig,
- pub mesh: MeshConfig,
- pub rpc: RpcConfig,
- pub rhi: RhiConfig,
- pub capability_bindings: Vec<CapabilityBindingConfig>,
-}
-
-#[derive(Debug, Default)]
-pub(crate) struct EnvFileValues(BTreeMap<String, String>);
-
-impl EnvFileValues {
- pub(crate) fn get(&self, key: &str) -> Option<&str> {
- self.0.get(key).map(String::as_str)
- }
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct CliConfigFile {
- output: Option<OutputFileConfig>,
- logging: Option<LoggingFileConfig>,
- account: Option<AccountFileConfig>,
- identity: Option<IdentityFileConfig>,
- transport: Option<TransportFileConfig>,
- signer: Option<SignerFileConfig>,
- myc: Option<MycFileConfig>,
- hyf: Option<HyfFileConfig>,
- mesh: Option<MeshFileConfig>,
- rpc: Option<RpcFileConfig>,
- rhi: Option<RhiFileConfig>,
- capability_binding: Option<Vec<CapabilityBindingFileConfig>>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct OutputFileConfig {
- format: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct LoggingFileConfig {
- filter: Option<String>,
- output_dir: Option<PathBuf>,
- stdout: Option<bool>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct AccountFileConfig {
- selector: Option<String>,
- secret: Option<AccountSecretFileConfig>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct AccountSecretFileConfig {
- backend: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct IdentityFileConfig {
- path: Option<PathBuf>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct TransportFileConfig {
- profile: Option<String>,
- nostr: Option<NostrTransportFileConfig>,
- reticulum: Option<ReticulumFileConfig>,
- radrootsd_execution: Option<RadrootsdExecutionFileConfig>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct NostrTransportFileConfig {
- relay_urls: Option<Vec<String>>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct ReticulumFileConfig {
- behavior: Option<String>,
- scope: Option<String>,
- agent_endpoint: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct RadrootsdExecutionFileConfig {
- url: Option<String>,
- token_file: Option<PathBuf>,
- token_secret_id: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct RpcFileConfig {
- url: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct RhiFileConfig {
- validator_set: Option<RhiValidatorSetFileConfig>,
- require_cryptographic_proof: Option<bool>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct RhiValidatorSetFileConfig {
- set_id: Option<String>,
- validator_pubkey: Option<String>,
- validator_set_addr: Option<String>,
- validator_set_event_id: Option<String>,
- valid_from: Option<u64>,
- valid_until: Option<u64>,
- protocol_contract_hash: Option<String>,
- operator_name: Option<String>,
- operator_contact: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct MycFileConfig {
- executable: Option<PathBuf>,
- status_timeout_ms: Option<u64>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct SignerFileConfig {
- backend: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct HyfFileConfig {
- enabled: Option<bool>,
- executable: Option<PathBuf>,
-}
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(default, deny_unknown_fields)]
-struct MeshFileConfig {
- scope: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct CapabilityBindingFileConfig {
- capability: String,
- provider: String,
- target_kind: String,
- target: String,
- managed_account_ref: Option<String>,
- signer_session_ref: Option<String>,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct CapabilityBindingSpec {
- capability_id: &'static str,
- provider_runtime_id: &'static str,
- binding_model: &'static str,
-}
-
-pub(crate) const SIGNER_REMOTE_NIP46_CAPABILITY: &str = "signer.remote_nip46";
-pub(crate) const INFERENCE_HYF_STDIO_CAPABILITY: &str = "inference.hyf_stdio";
-const CAPABILITY_BINDING_SPECS: &[CapabilityBindingSpec] = &[
- CapabilityBindingSpec {
- capability_id: SIGNER_REMOTE_NIP46_CAPABILITY,
- provider_runtime_id: "myc",
- binding_model: "session_authorized_remote_signer",
- },
- CapabilityBindingSpec {
- capability_id: INFERENCE_HYF_STDIO_CAPABILITY,
- provider_runtime_id: "hyf",
- binding_model: "stdio_service",
- },
-];
-
-pub(crate) trait Environment {
- fn var(&self, key: &str) -> Option<String>;
- fn current_dir(&self) -> Result<PathBuf, RuntimeError>;
- fn path_resolver(&self) -> RadrootsPathResolver;
- fn stdin_is_tty(&self) -> bool;
- fn stdout_is_tty(&self) -> bool;
-}
-
-pub struct SystemEnvironment;
-
-impl Environment for SystemEnvironment {
- fn var(&self, key: &str) -> Option<String> {
- std::env::var(key).ok()
- }
-
- fn current_dir(&self) -> Result<PathBuf, RuntimeError> {
- std::env::current_dir().map_err(|err| {
- RuntimeError::Config(format!("failed to resolve current directory: {err}"))
- })
- }
-
- fn path_resolver(&self) -> RadrootsPathResolver {
- RadrootsPathResolver::current()
- }
-
- fn stdin_is_tty(&self) -> bool {
- std::io::stdin().is_terminal()
- }
-
- fn stdout_is_tty(&self) -> bool {
- std::io::stdout().is_terminal()
- }
-}
-
-impl RuntimeConfig {
- pub fn from_system(args: &RuntimeInvocationArgs) -> Result<Self, RuntimeError> {
- let system = SystemEnvironment;
- let env_file_path = resolve_env_file_path(args, &system);
- let env_file = load_env_file_values(env_file_path.as_deref())?;
- Self::resolve_with_env_file(args, &system, &env_file)
- }
-
- fn resolve_with_env_file(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- ) -> Result<Self, RuntimeError> {
- let paths = resolve_paths(env, env_file)?;
- let workspace_config = paths
- .workspace_config_path
- .as_deref()
- .map(load_cli_config_file)
- .transpose()?
- .flatten();
- let app_config = load_cli_config_file(paths.app_config_path.as_path())?;
- let account_secret_backend = resolve_account_secret_backend(
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?
- .unwrap_or(RadrootsSecretBackend::HostVault(
- RadrootsHostVaultPolicy::desktop(),
- ));
- let output = OutputConfig {
- format: resolve_output_format(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- verbosity: resolve_verbosity(args)?,
- dry_run: args.dry_run,
- };
- let logging = LoggingConfig {
- filter: resolve_logging_filter(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- ),
- directory: resolve_logging_directory(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- paths.app_logs_root.as_path(),
- ),
- stdout: resolve_logging_stdout(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- };
- validate_logging_output_contract(&output, &logging)?;
- let transport = resolve_transport_config(
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?;
- Ok(Self {
- capability_bindings: resolve_capability_bindings(
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- output,
- interaction: resolve_interaction_config(args, env),
- paths: paths.clone(),
- logging,
- account: AccountConfig {
- selector: args
- .account
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_ACCOUNT_SELECTOR]))
- .or_else(|| {
- app_config
- .as_ref()
- .and_then(|config| config.account.as_ref())
- .and_then(|account| account.selector.clone())
- })
- .or_else(|| {
- workspace_config
- .as_ref()
- .and_then(|config| config.account.as_ref())
- .and_then(|account| account.selector.clone())
- }),
- store_path: paths
- .shared_accounts_data_root
- .join(DEFAULT_SHARED_ACCOUNTS_STORE_FILE),
- secrets_dir: paths.shared_accounts_secrets_root.clone(),
- secret_backend: account_secret_backend,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: CLI_DEFAULT_SECRET_BACKEND.to_owned(),
- allowed_backends: CLI_ALLOWED_SHARED_SECRET_BACKENDS
- .iter()
- .map(|value| (*value).to_owned())
- .collect(),
- host_vault_policy: Some(CLI_HOST_VAULT_POLICY.to_owned()),
- uses_protected_store: CLI_USES_PROTECTED_STORE,
- },
- identity: IdentityConfig {
- path: args
- .identity_path
- .clone()
- .or_else(|| {
- env_value(env, env_file, &[ENV_CLI_IDENTITY_PATH]).map(PathBuf::from)
- })
- .or_else(|| {
- app_config
- .as_ref()
- .and_then(|config| config.identity.as_ref())
- .and_then(|identity| identity.path.clone())
- })
- .or_else(|| {
- workspace_config
- .as_ref()
- .and_then(|config| config.identity.as_ref())
- .and_then(|identity| identity.path.clone())
- })
- .unwrap_or_else(|| paths.default_identity_path.clone()),
- },
- signer: resolve_signer_config(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- transport,
- local: LocalConfig {
- root: paths.app_data_root.join(DEFAULT_LOCAL_STATE_DIR),
- replica_store_path: paths
- .app_data_root
- .join(DEFAULT_LOCAL_STATE_DIR)
- .join(DEFAULT_LOCAL_DB_FILE),
- backups_dir: paths
- .app_data_root
- .join(DEFAULT_LOCAL_STATE_DIR)
- .join(DEFAULT_LOCAL_BACKUPS_DIR),
- exports_dir: paths
- .app_data_root
- .join(DEFAULT_LOCAL_STATE_DIR)
- .join(DEFAULT_LOCAL_EXPORTS_DIR),
- },
- myc: resolve_myc_config(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- hyf: HyfConfig {
- enabled: resolve_hyf_enabled(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- executable: resolve_hyf_executable(
- args,
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- ),
- },
- mesh: resolve_mesh_config(
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- rpc: resolve_rpc_config(
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- rhi: resolve_rhi_config(
- env,
- env_file,
- app_config.as_ref(),
- workspace_config.as_ref(),
- )?,
- })
- }
-
- #[cfg(test)]
- pub fn inspect_capability_bindings(&self) -> Vec<CapabilityBindingInspection> {
- CAPABILITY_BINDING_SPECS
- .iter()
- .map(|spec| {
- if let Some(binding) = self
- .capability_bindings
- .iter()
- .find(|binding| binding.capability_id == spec.capability_id)
- {
- return CapabilityBindingInspection {
- capability_id: binding.capability_id.clone(),
- provider_runtime_id: binding.provider_runtime_id.clone(),
- binding_model: binding.binding_model.clone(),
- state: CapabilityBindingInspectionState::Configured,
- source: binding.source.as_str().to_owned(),
- target_kind: Some(binding.target_kind.as_str().to_owned()),
- target: Some(binding.target.clone()),
- managed_account_ref: binding.managed_account_ref.clone(),
- signer_session_ref: binding.signer_session_ref.clone(),
- };
- }
-
- let (state, source) = match spec.capability_id {
- SIGNER_REMOTE_NIP46_CAPABILITY
- if matches!(self.signer.backend, SignerBackend::Local) =>
- {
- (
- CapabilityBindingInspectionState::Disabled,
- "independent local signer mode".to_owned(),
- )
- }
- INFERENCE_HYF_STDIO_CAPABILITY if !self.hyf.enabled => (
- CapabilityBindingInspectionState::Disabled,
- "hyf disabled by config".to_owned(),
- ),
- _ => (
- CapabilityBindingInspectionState::NotConfigured,
- "no explicit capability binding".to_owned(),
- ),
- };
-
- CapabilityBindingInspection {
- capability_id: spec.capability_id.to_owned(),
- provider_runtime_id: spec.provider_runtime_id.to_owned(),
- binding_model: spec.binding_model.to_owned(),
- state,
- source,
- target_kind: None,
- target: None,
- managed_account_ref: None,
- signer_session_ref: None,
- }
- })
- .collect()
- }
-
- pub fn capability_binding(&self, capability_id: &str) -> Option<&CapabilityBindingConfig> {
- self.capability_bindings
- .iter()
- .find(|binding| binding.capability_id == capability_id)
- }
-}
-
-fn load_cli_config_file(path: &Path) -> Result<Option<CliConfigFile>, RuntimeError> {
- if !path.exists() {
- return Ok(None);
- }
-
- let raw = fs::read_to_string(path).map_err(|err| {
- RuntimeError::Config(format!(
- "failed to read config file {}: {err}",
- path.display()
- ))
- })?;
-
- if raw.trim().is_empty() {
- return Ok(Some(CliConfigFile::default()));
- }
-
- toml::from_str::<CliConfigFile>(&raw)
- .map(Some)
- .map_err(|err| {
- RuntimeError::Config(format!(
- "failed to parse config file {}: {err}",
- path.display()
- ))
- })
-}
-
-fn resolve_rpc_config(
- _env: &dyn Environment,
- _env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<RpcConfig, RuntimeError> {
- let url = user_config
- .and_then(|config| config.rpc.as_ref())
- .and_then(|rpc| rpc.url.clone())
- .or_else(|| {
- workspace_config
- .and_then(|config| config.rpc.as_ref())
- .and_then(|rpc| rpc.url.clone())
- })
- .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned());
-
- Ok(RpcConfig {
- url: validate_rpc_url(url.as_str())?,
- })
-}
-
-fn resolve_transport_config(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<TransportConfig, RuntimeError> {
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_TRANSPORT_PROFILE]) {
- let profile = parse_transport_profile_kind(key.as_str(), value.as_str())?;
- let reticulum_behavior = env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR])
- .map(|value| {
- parse_reticulum_behavior(ENV_CLI_TRANSPORT_RETICULUM_BEHAVIOR, value.as_str())
- })
- .transpose()?
- .unwrap_or(ReticulumBehavior::RejectDeliveryAttempts);
- let reticulum_scope = env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_SCOPE])
- .map(|value| parse_reticulum_scope(ENV_CLI_TRANSPORT_RETICULUM_SCOPE, value.as_str()))
- .transpose()?
- .unwrap_or_else(|| RADROOTS_RETICULUM_SCOPE_ID.to_owned());
- let reticulum_agent_endpoint =
- env_value(env, env_file, &[ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT])
- .map(|value| {
- parse_reticulum_agent_endpoint(
- ENV_CLI_TRANSPORT_RETICULUM_AGENT_ENDPOINT,
- value.as_str(),
- )
- })
- .transpose()?;
- return Ok(TransportConfig {
- profile,
- source: TransportConfigSource::Environment,
- nostr_relay_urls: env_value(env, env_file, &[ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS])
- .map(|value| {
- parse_relay_env_value(value.as_str(), ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS)
- })
- .transpose()?
- .unwrap_or_default(),
- reticulum_behavior,
- reticulum_scope,
- reticulum_agent_endpoint,
- radrootsd_execution: resolve_radrootsd_execution_env(env, env_file)?,
- });
- }
-
- if let Some(transport) = user_config.and_then(|config| config.transport.as_ref()) {
- return resolve_transport_file_config(
- transport,
- TransportConfigSource::UserConfig,
- "user config [transport]",
- );
- }
-
- if let Some(transport) = workspace_config.and_then(|config| config.transport.as_ref()) {
- return resolve_transport_file_config(
- transport,
- TransportConfigSource::WorkspaceConfig,
- "workspace config [transport]",
- );
- }
-
- Ok(TransportConfig {
- profile: TransportProfileKind::LocalOnly,
- source: TransportConfigSource::Defaults,
- nostr_relay_urls: Vec::new(),
- reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts,
- reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(),
- reticulum_agent_endpoint: None,
- radrootsd_execution: RadrootsdExecutionConfig::default(),
- })
-}
-
-fn resolve_transport_file_config(
- transport: &TransportFileConfig,
- source: TransportConfigSource,
- source_label: &str,
-) -> Result<TransportConfig, RuntimeError> {
- let Some(profile_value) = transport.profile.as_deref() else {
- if transport.nostr.is_some()
- || transport.reticulum.is_some()
- || transport.radrootsd_execution.is_some()
- {
- return Err(RuntimeError::Config(format!(
- "{source_label}.profile must be set when profile-specific transport tables are present"
- )));
- }
- return Ok(TransportConfig {
- profile: TransportProfileKind::LocalOnly,
- source,
- nostr_relay_urls: Vec::new(),
- reticulum_behavior: ReticulumBehavior::RejectDeliveryAttempts,
- reticulum_scope: RADROOTS_RETICULUM_SCOPE_ID.to_owned(),
- reticulum_agent_endpoint: None,
- radrootsd_execution: RadrootsdExecutionConfig::default(),
- });
- };
- let profile =
- parse_transport_profile_kind(format!("{source_label}.profile").as_str(), profile_value)?;
- let nostr_relay_urls = transport
- .nostr
- .as_ref()
- .and_then(|nostr| nostr.relay_urls.clone())
- .map(|urls| normalize_relay_urls(urls, format!("{source_label}.nostr.relay_urls").as_str()))
- .transpose()?
- .unwrap_or_default();
- let reticulum_behavior = transport
- .reticulum
- .as_ref()
- .and_then(|reticulum| reticulum.behavior.as_deref())
- .map(|value| {
- parse_reticulum_behavior(format!("{source_label}.reticulum.behavior").as_str(), value)
- })
- .transpose()?
- .unwrap_or(ReticulumBehavior::RejectDeliveryAttempts);
- let reticulum_scope = transport
- .reticulum
- .as_ref()
- .and_then(|reticulum| reticulum.scope.as_deref())
- .map(|value| {
- parse_reticulum_scope(format!("{source_label}.reticulum.scope").as_str(), value)
- })
- .transpose()?
- .unwrap_or_else(|| RADROOTS_RETICULUM_SCOPE_ID.to_owned());
- let reticulum_agent_endpoint = transport
- .reticulum
- .as_ref()
- .and_then(|reticulum| reticulum.agent_endpoint.as_deref())
- .map(|value| {
- parse_reticulum_agent_endpoint(
- format!("{source_label}.reticulum.agent_endpoint").as_str(),
- value,
- )
- })
- .transpose()?;
- Ok(TransportConfig {
- profile,
- source,
- nostr_relay_urls,
- reticulum_behavior,
- reticulum_scope,
- reticulum_agent_endpoint,
- radrootsd_execution: resolve_radrootsd_execution_file(
- transport.radrootsd_execution.as_ref(),
- source_label,
- )?,
- })
-}
-
-fn resolve_radrootsd_execution_env(
- env: &dyn Environment,
- env_file: &EnvFileValues,
-) -> Result<RadrootsdExecutionConfig, RuntimeError> {
- let url = env_value(env, env_file, &[ENV_CLI_RADROOTSD_EXECUTION_URL])
- .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned());
- let token_file =
- env_value(env, env_file, &[ENV_CLI_RADROOTSD_EXECUTION_TOKEN_FILE]).map(PathBuf::from);
- let token_secret_id = env_value(
- env,
- env_file,
- &[ENV_CLI_RADROOTSD_EXECUTION_TOKEN_SECRET_ID],
- );
- validate_radrootsd_execution_token_source(
- token_file.as_ref(),
- token_secret_id.as_deref(),
- "environment radrootsd execution",
- )?;
- Ok(RadrootsdExecutionConfig {
- url: validate_rpc_url(url.as_str())?,
- token_file,
- token_secret_id,
- })
-}
-
-fn resolve_radrootsd_execution_file(
- radrootsd_execution: Option<&RadrootsdExecutionFileConfig>,
- source_label: &str,
-) -> Result<RadrootsdExecutionConfig, RuntimeError> {
- let url = radrootsd_execution
- .and_then(|execution| execution.url.clone())
- .unwrap_or_else(|| DEFAULT_RPC_URL.to_owned());
- let token_file = radrootsd_execution.and_then(|execution| execution.token_file.clone());
- let token_secret_id =
- radrootsd_execution.and_then(|execution| execution.token_secret_id.clone());
- validate_radrootsd_execution_token_source(
- token_file.as_ref(),
- token_secret_id.as_deref(),
- format!("{source_label}.radrootsd_execution").as_str(),
- )?;
- Ok(RadrootsdExecutionConfig {
- url: validate_rpc_url(url.as_str()).map_err(|error| match error {
- RuntimeError::Config(message) => RuntimeError::Config(format!(
- "{source_label}.radrootsd_execution.url is invalid: {message}"
- )),
- other => other,
- })?,
- token_file,
- token_secret_id,
- })
-}
-
-fn validate_radrootsd_execution_token_source(
- token_file: Option<&PathBuf>,
- token_secret_id: Option<&str>,
- source_label: &str,
-) -> Result<(), RuntimeError> {
- if token_file.is_some_and(|path| path.as_os_str().is_empty()) {
- return Err(RuntimeError::Config(format!(
- "{source_label}.token_file must not be empty"
- )));
- }
- if token_secret_id.is_some_and(|secret_id| secret_id.trim().is_empty()) {
- return Err(RuntimeError::Config(format!(
- "{source_label}.token_secret_id must not be empty"
- )));
- }
- if token_file.is_some() && token_secret_id.is_some() {
- return Err(RuntimeError::Config(format!(
- "{source_label} cannot set both token_file and token_secret_id"
- )));
- }
- Ok(())
-}
-
-fn resolve_rhi_config(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<RhiConfig, RuntimeError> {
- let validator_set =
- resolve_rhi_validator_set_config(env, env_file, user_config, workspace_config)?;
- let require_cryptographic_proof =
- if let Some(value) = env_value(env, env_file, &[ENV_CLI_RHI_REQUIRE_CRYPTOGRAPHIC_PROOF]) {
- parse_bool_env(ENV_CLI_RHI_REQUIRE_CRYPTOGRAPHIC_PROOF, value.as_str())?
- } else if let Some(value) = user_config
- .and_then(|config| config.rhi.as_ref())
- .and_then(|rhi| rhi.require_cryptographic_proof)
- {
- value
- } else {
- workspace_config
- .and_then(|config| config.rhi.as_ref())
- .and_then(|rhi| rhi.require_cryptographic_proof)
- .unwrap_or_default()
- };
-
- Ok(RhiConfig {
- validator_set,
- require_cryptographic_proof,
- })
-}
-
-fn resolve_rhi_validator_set_config(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<Option<RhiValidatorSetConfig>, RuntimeError> {
- let env_keys = [
- ENV_CLI_RHI_VALIDATOR_SET_ID,
- ENV_CLI_RHI_VALIDATOR_PUBKEY,
- ENV_CLI_RHI_VALIDATOR_SET_ADDR,
- ENV_CLI_RHI_VALIDATOR_SET_EVENT_ID,
- ENV_CLI_RHI_VALIDATOR_SET_VALID_FROM,
- ENV_CLI_RHI_VALIDATOR_SET_VALID_UNTIL,
- ENV_CLI_RHI_VALIDATOR_SET_PROTOCOL_CONTRACT_HASH,
- ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_NAME,
- ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_CONTACT,
- ];
- if env_keys
- .iter()
- .any(|key| env_value(env, env_file, &[*key]).is_some())
- {
- return Ok(Some(validate_rhi_validator_set_config(
- RhiValidatorSetConfig {
- set_id: required_env_value(env, env_file, ENV_CLI_RHI_VALIDATOR_SET_ID)?,
- validator_pubkey: required_env_value(env, env_file, ENV_CLI_RHI_VALIDATOR_PUBKEY)?,
- validator_set_addr: required_env_value(
- env,
- env_file,
- ENV_CLI_RHI_VALIDATOR_SET_ADDR,
- )?,
- validator_set_event_id: required_env_value(
- env,
- env_file,
- ENV_CLI_RHI_VALIDATOR_SET_EVENT_ID,
- )?,
- valid_from: parse_u64_value(
- ENV_CLI_RHI_VALIDATOR_SET_VALID_FROM,
- required_env_value(env, env_file, ENV_CLI_RHI_VALIDATOR_SET_VALID_FROM)?
- .as_str(),
- )
- .map_err(|err| RuntimeError::Config(err.to_string()))?,
- valid_until: parse_u64_value(
- ENV_CLI_RHI_VALIDATOR_SET_VALID_UNTIL,
- required_env_value(env, env_file, ENV_CLI_RHI_VALIDATOR_SET_VALID_UNTIL)?
- .as_str(),
- )
- .map_err(|err| RuntimeError::Config(err.to_string()))?,
- protocol_contract_hash: required_env_value(
- env,
- env_file,
- ENV_CLI_RHI_VALIDATOR_SET_PROTOCOL_CONTRACT_HASH,
- )?,
- operator_name: required_env_value(
- env,
- env_file,
- ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_NAME,
- )?,
- operator_contact: env_value(
- env,
- env_file,
- &[ENV_CLI_RHI_VALIDATOR_SET_OPERATOR_CONTACT],
- ),
- },
- "environment RHI validator_set",
- )?));
- }
- if let Some(config) = user_config
- .and_then(|config| config.rhi.as_ref())
- .and_then(|rhi| rhi.validator_set.as_ref())
- {
- return Ok(Some(rhi_validator_set_from_file_config(
- config,
- "user config [rhi.validator_set]",
- )?));
- }
- if let Some(config) = workspace_config
- .and_then(|config| config.rhi.as_ref())
- .and_then(|rhi| rhi.validator_set.as_ref())
- {
- return Ok(Some(rhi_validator_set_from_file_config(
- config,
- "workspace config [rhi.validator_set]",
- )?));
- }
- Ok(None)
-}
-
-fn required_env_value(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- key: &'static str,
-) -> Result<String, RuntimeError> {
- env_value(env, env_file, &[key]).ok_or_else(|| {
- RuntimeError::Config(format!(
- "{key} is required when any RHI validator set environment key is set"
- ))
- })
-}
-
-fn rhi_validator_set_from_file_config(
- config: &RhiValidatorSetFileConfig,
- source: &'static str,
-) -> Result<RhiValidatorSetConfig, RuntimeError> {
- validate_rhi_validator_set_config(
- RhiValidatorSetConfig {
- set_id: required_file_value(config.set_id.clone(), source, "set_id")?,
- validator_pubkey: required_file_value(
- config.validator_pubkey.clone(),
- source,
- "validator_pubkey",
- )?,
- validator_set_addr: required_file_value(
- config.validator_set_addr.clone(),
- source,
- "validator_set_addr",
- )?,
- validator_set_event_id: required_file_value(
- config.validator_set_event_id.clone(),
- source,
- "validator_set_event_id",
- )?,
- valid_from: config
- .valid_from
- .ok_or_else(|| missing_file_value(source, "valid_from"))?,
- valid_until: config
- .valid_until
- .ok_or_else(|| missing_file_value(source, "valid_until"))?,
- protocol_contract_hash: required_file_value(
- config.protocol_contract_hash.clone(),
- source,
- "protocol_contract_hash",
- )?,
- operator_name: required_file_value(
- config.operator_name.clone(),
- source,
- "operator_name",
- )?,
- operator_contact: config.operator_contact.clone(),
- },
- source,
- )
-}
-
-fn required_file_value(
- value: Option<String>,
- source: &'static str,
- field: &'static str,
-) -> Result<String, RuntimeError> {
- value.ok_or_else(|| missing_file_value(source, field))
-}
-
-fn missing_file_value(source: &'static str, field: &'static str) -> RuntimeError {
- RuntimeError::Config(format!("{source}.{field} is required"))
-}
-
-fn validate_rhi_validator_set_config(
- mut config: RhiValidatorSetConfig,
- source: &'static str,
-) -> Result<RhiValidatorSetConfig, RuntimeError> {
- config.set_id = validate_uuidv7(config.set_id.as_str(), source, "set_id")?;
- config.validator_pubkey = validate_pubkey(
- config.validator_pubkey.as_str(),
- format!("{source}.validator_pubkey").as_str(),
- )?;
- config.validator_set_event_id = validate_event_id(
- config.validator_set_event_id.as_str(),
- format!("{source}.validator_set_event_id").as_str(),
- )?;
- config.protocol_contract_hash = validate_hash32(
- config.protocol_contract_hash.as_str(),
- format!("{source}.protocol_contract_hash").as_str(),
- )?;
- config.validator_set_addr = validate_validator_set_address(
- config.validator_set_addr.as_str(),
- config.set_id.as_str(),
- format!("{source}.validator_set_addr").as_str(),
- )?;
- if config.valid_from >= config.valid_until {
- return Err(RuntimeError::Config(format!(
- "{source}.valid_from must be less than {source}.valid_until"
- )));
- }
- config.operator_name = validate_required_text(
- config.operator_name.as_str(),
- format!("{source}.operator_name").as_str(),
- )?;
- if let Some(operator_contact) = config.operator_contact.as_ref() {
- validate_required_text(
- operator_contact.as_str(),
- format!("{source}.operator_contact").as_str(),
- )?;
- }
- Ok(config)
-}
-
-fn validate_pubkey(value: &str, source: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.len() != 64 || !trimmed.chars().all(|char| char.is_ascii_hexdigit()) {
- return Err(RuntimeError::Config(format!(
- "{source} must contain 64-character hex Nostr public keys"
- )));
- }
- Ok(trimmed.to_ascii_lowercase())
-}
-
-fn validate_event_id(value: &str, source: &str) -> Result<String, RuntimeError> {
- validate_pubkey(value, source)
-}
-
-fn validate_hash32(value: &str, source: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- let Some(hex) = trimmed.strip_prefix("0x") else {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 0x-prefixed 32-byte lowercase hex digest"
- )));
- };
- if hex.len() != 64 || !hex.chars().all(|char| char.is_ascii_hexdigit()) {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 0x-prefixed 32-byte lowercase hex digest"
- )));
- }
- Ok(format!("0x{}", hex.to_ascii_lowercase()))
-}
-
-fn validate_uuidv7(value: &str, source: &str, field: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim().to_ascii_lowercase();
- let bytes = trimmed.as_bytes();
- if bytes.len() != 36
- || bytes[8] != b'-'
- || bytes[13] != b'-'
- || bytes[18] != b'-'
- || bytes[23] != b'-'
- || bytes[14] != b'7'
- || !matches!(bytes[19], b'8'..=b'9' | b'a'..=b'b')
- {
- return Err(RuntimeError::Config(format!(
- "{source}.{field} must be a UUIDv7"
- )));
- }
- for (index, byte) in bytes.iter().enumerate() {
- if matches!(index, 8 | 13 | 18 | 23) {
- continue;
- }
- if !byte.is_ascii_digit() && !(b'a'..=b'f').contains(byte) {
- return Err(RuntimeError::Config(format!(
- "{source}.{field} must be a UUIDv7"
- )));
- }
- }
- Ok(trimmed)
-}
-
-fn validate_validator_set_address(
- value: &str,
- expected_set_id: &str,
- source: &str,
-) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- let mut parts = trimmed.split(':');
- let Some(kind) = parts.next() else {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 30381 address"
- )));
- };
- let Some(pubkey) = parts.next() else {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 30381 address"
- )));
- };
- let Some(set_id) = parts.next() else {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 30381 address"
- )));
- };
- if parts.next().is_some() || kind != "30381" || set_id != expected_set_id {
- return Err(RuntimeError::Config(format!(
- "{source} must be a 30381 address"
- )));
- }
- let pubkey = validate_pubkey(pubkey, source)?;
- Ok(format!("30381:{pubkey}:{set_id}"))
-}
-
-fn validate_required_text(value: &str, source: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return Err(RuntimeError::Config(format!("{source} must not be empty")));
- }
- Ok(trimmed.to_owned())
-}
-
-fn resolve_capability_bindings(
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<Vec<CapabilityBindingConfig>, RuntimeError> {
- let workspace = resolve_file_capability_bindings(
- workspace_config.and_then(|config| config.capability_binding.as_deref()),
- CapabilityBindingSource::WorkspaceConfig,
- )?;
- let user = resolve_file_capability_bindings(
- user_config.and_then(|config| config.capability_binding.as_deref()),
- CapabilityBindingSource::UserConfig,
- )?;
-
- let mut merged = BTreeMap::new();
- for binding in workspace.into_iter().chain(user) {
- merged.insert(binding.capability_id.clone(), binding);
- }
-
- Ok(CAPABILITY_BINDING_SPECS
- .iter()
- .filter_map(|spec| merged.remove(spec.capability_id))
- .collect())
-}
-
-fn resolve_file_capability_bindings(
- bindings: Option<&[CapabilityBindingFileConfig]>,
- source: CapabilityBindingSource,
-) -> Result<Vec<CapabilityBindingConfig>, RuntimeError> {
- let Some(bindings) = bindings else {
- return Ok(Vec::new());
- };
-
- let mut seen = BTreeMap::new();
- let mut resolved = Vec::with_capacity(bindings.len());
-
- for binding in bindings {
- let capability = binding.capability.trim();
- let provider = binding.provider.trim();
- let Some(spec) = capability_binding_spec(capability) else {
- return Err(RuntimeError::Config(format!(
- "unknown capability_binding capability `{capability}`"
- )));
- };
- if provider != spec.provider_runtime_id {
- return Err(RuntimeError::Config(format!(
- "capability_binding `{capability}` must use provider `{}`, got `{provider}`",
- spec.provider_runtime_id
- )));
- }
- if seen.insert(spec.capability_id.to_owned(), ()).is_some() {
- return Err(RuntimeError::Config(format!(
- "capability_binding `{capability}` is duplicated in one config file"
- )));
- }
-
- let target = binding.target.trim();
- if target.is_empty() {
- return Err(RuntimeError::Config(format!(
- "capability_binding `{capability}` target must not be empty"
- )));
- }
-
- let managed_account_ref = normalize_binding_ref(
- binding
- .managed_account_ref
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty()),
- );
- let signer_session_ref = normalize_binding_ref(
- binding
- .signer_session_ref
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty()),
- );
- if spec.capability_id != SIGNER_REMOTE_NIP46_CAPABILITY
- && (managed_account_ref.is_some() || signer_session_ref.is_some())
- {
- return Err(RuntimeError::Config(format!(
- "capability_binding `{capability}` may not set managed_account_ref or signer_session_ref"
- )));
- }
-
- resolved.push(CapabilityBindingConfig {
- capability_id: spec.capability_id.to_owned(),
- provider_runtime_id: spec.provider_runtime_id.to_owned(),
- binding_model: spec.binding_model.to_owned(),
- target_kind: parse_capability_binding_target_kind(
- binding.target_kind.as_str(),
- spec.capability_id,
- )?,
- target: target.to_owned(),
- managed_account_ref,
- signer_session_ref,
- source,
- });
- }
-
- Ok(resolved)
-}
-
-fn capability_binding_spec(capability_id: &str) -> Option<CapabilityBindingSpec> {
- CAPABILITY_BINDING_SPECS
- .iter()
- .copied()
- .find(|spec| spec.capability_id == capability_id)
-}
-
-fn parse_capability_binding_target_kind(
- value: &str,
- capability_id: &str,
-) -> Result<CapabilityBindingTargetKind, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "managed_instance" => Ok(CapabilityBindingTargetKind::ManagedInstance),
- "explicit_endpoint" => Ok(CapabilityBindingTargetKind::ExplicitEndpoint),
- other => Err(RuntimeError::Config(format!(
- "capability_binding `{capability_id}` target_kind must be `managed_instance` or `explicit_endpoint`, got `{other}`"
- ))),
- }
-}
-
-fn normalize_binding_ref(value: Option<&str>) -> Option<String> {
- value.map(ToOwned::to_owned)
-}
-
-fn resolve_signer_config(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<SignerConfig, RuntimeError> {
- let backend = if let Some(value) = args.signer.clone() {
- parse_signer_mode("internal invocation signer mode", value)?
- } else if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_SIGNER_BACKEND]) {
- parse_signer_mode(key.as_str(), value)?
- } else if let Some(value) = user_config
- .and_then(|config| config.signer.as_ref())
- .and_then(|signer| signer.backend.clone())
- {
- parse_signer_mode("user config [signer].backend", value)?
- } else if let Some(value) = workspace_config
- .and_then(|config| config.signer.as_ref())
- .and_then(|signer| signer.backend.clone())
- {
- parse_signer_mode("workspace config [signer].backend", value)?
- } else {
- SignerBackend::Local
- };
-
- Ok(SignerConfig { backend })
-}
-
-fn resolve_myc_config(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<MycConfig, RuntimeError> {
- let executable = args
- .myc_executable
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_MYC_EXECUTABLE]).map(PathBuf::from))
- .or_else(|| {
- user_config
- .and_then(|config| config.myc.as_ref())
- .and_then(|myc| myc.executable.clone())
- })
- .or_else(|| {
- workspace_config
- .and_then(|config| config.myc.as_ref())
- .and_then(|myc| myc.executable.clone())
- })
- .unwrap_or_else(|| PathBuf::from("myc"));
-
- Ok(MycConfig {
- executable,
- status_timeout_ms: resolve_myc_status_timeout_ms(
- args,
- env,
- env_file,
- user_config,
- workspace_config,
- )?,
- })
-}
-
-fn resolve_myc_status_timeout_ms(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<u64, RuntimeError> {
- if let Some(value) = args.myc_status_timeout_ms {
- return validate_myc_status_timeout_ms("--myc-status-timeout-ms", value);
- }
-
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_MYC_STATUS_TIMEOUT_MS]) {
- let parsed = parse_u64_value(key.as_str(), value.as_str())
- .map_err(|err| RuntimeError::Config(err.to_string()))?;
- return validate_myc_status_timeout_ms(key.as_str(), parsed);
- }
-
- if let Some(value) = user_config
- .and_then(|config| config.myc.as_ref())
- .and_then(|myc| myc.status_timeout_ms)
- {
- return validate_myc_status_timeout_ms("user config [myc].status_timeout_ms", value);
- }
-
- if let Some(value) = workspace_config
- .and_then(|config| config.myc.as_ref())
- .and_then(|myc| myc.status_timeout_ms)
- {
- return validate_myc_status_timeout_ms("workspace config [myc].status_timeout_ms", value);
- }
-
- Ok(DEFAULT_MYC_STATUS_TIMEOUT_MS)
-}
-
-fn validate_myc_status_timeout_ms(source: &str, value: u64) -> Result<u64, RuntimeError> {
- if value == 0 {
- return Err(RuntimeError::Config(format!(
- "{source} must be greater than zero"
- )));
- }
- Ok(value)
-}
-
-fn resolve_hyf_enabled(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<bool, RuntimeError> {
- match (args.hyf_enabled, args.no_hyf_enabled) {
- (true, true) => {
- return Err(RuntimeError::Config(
- "flags --hyf-enabled and --no-hyf-enabled cannot be used together".to_owned(),
- ));
- }
- (true, false) => return Ok(true),
- (false, true) => return Ok(false),
- (false, false) => {}
- }
-
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_HYF_ENABLED]) {
- return parse_bool_env(key.as_str(), value.as_str());
- }
-
- if let Some(enabled) = user_config
- .and_then(|config| config.hyf.as_ref())
- .and_then(|hyf| hyf.enabled)
- {
- return Ok(enabled);
- }
-
- if let Some(enabled) = workspace_config
- .and_then(|config| config.hyf.as_ref())
- .and_then(|hyf| hyf.enabled)
- {
- return Ok(enabled);
- }
-
- Ok(false)
-}
-
-fn resolve_hyf_executable(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> PathBuf {
- args.hyf_executable
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_HYF_EXECUTABLE]).map(PathBuf::from))
- .or_else(|| {
- user_config
- .and_then(|config| config.hyf.as_ref())
- .and_then(|hyf| hyf.executable.clone())
- })
- .or_else(|| {
- workspace_config
- .and_then(|config| config.hyf.as_ref())
- .and_then(|hyf| hyf.executable.clone())
- })
- .unwrap_or_else(|| PathBuf::from(DEFAULT_HYF_EXECUTABLE))
-}
-
-fn resolve_mesh_config(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<MeshConfig, RuntimeError> {
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_MESH_SCOPE]) {
- return Ok(MeshConfig {
- scope: parse_mesh_scope(key.as_str(), value.as_str())?,
- source: MeshConfigSource::Environment,
- });
- }
- if let Some(value) = user_config
- .and_then(|config| config.mesh.as_ref())
- .and_then(|mesh| mesh.scope.as_deref())
- {
- return Ok(MeshConfig {
- scope: parse_mesh_scope("user config [mesh].scope", value)?,
- source: MeshConfigSource::UserConfig,
- });
- }
- if let Some(value) = workspace_config
- .and_then(|config| config.mesh.as_ref())
- .and_then(|mesh| mesh.scope.as_deref())
- {
- return Ok(MeshConfig {
- scope: parse_mesh_scope("workspace config [mesh].scope", value)?,
- source: MeshConfigSource::WorkspaceConfig,
- });
- }
- Ok(MeshConfig {
- scope: MeshScope::Disabled,
- source: MeshConfigSource::Defaults,
- })
-}
-
-fn parse_mesh_scope(source: &str, value: &str) -> Result<MeshScope, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "disabled" => Ok(MeshScope::Disabled),
- "local" => Ok(MeshScope::Local),
- other => Err(RuntimeError::Config(format!(
- "{source} must be `disabled` or `local`, got `{other}`"
- ))),
- }
-}
-
-fn validate_rpc_url(value: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return Err(RuntimeError::Config("rpc url must not be empty".to_owned()));
- }
- let parsed = Url::parse(trimmed)
- .map_err(|err| RuntimeError::Config(format!("rpc url `{trimmed}` is invalid: {err}")))?;
- if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
- return Err(RuntimeError::Config(format!(
- "rpc url must use http or https, got `{trimmed}`"
- )));
- }
- Ok(trimmed.to_owned())
-}
-
-fn parse_relay_env_value(value: &str, key: &str) -> Result<Vec<String>, RuntimeError> {
- let entries = value
- .split(',')
- .map(str::trim)
- .map(ToOwned::to_owned)
- .collect::<Vec<_>>();
-
- if entries.is_empty() {
- return Err(RuntimeError::Config(format!(
- "{key} must contain at least one websocket relay url"
- )));
- }
-
- normalize_relay_urls(entries, key)
-}
-
-fn normalize_relay_urls(values: Vec<String>, source: &str) -> Result<Vec<String>, RuntimeError> {
- let mut normalized = Vec::new();
- for value in values {
- let relay = validate_relay_url(value.as_str(), source)?;
- if !normalized.iter().any(|existing| existing == &relay) {
- normalized.push(relay);
- }
- }
- Ok(normalized)
-}
-
-fn validate_relay_url(value: &str, source: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return Err(RuntimeError::Config(format!(
- "{source} contains an empty relay url"
- )));
- }
- let relay = if trimmed.starts_with("ws://") {
- radroots_transport::Target::nostr_relay(trimmed)
- .map(|target| target.uri().as_str().to_owned())
- .map_err(|error| RelayTransportError::InvalidRelayUrl {
- url: trimmed.to_owned(),
- reason: error.to_string(),
- })
- } else {
- RelayUrl::parse(trimmed, nostr_relay_url_policy_for_url(trimmed))
- .map(|relay| relay.as_str().to_owned())
- };
- relay.map_err(|error| match error {
- RelayTransportError::RelaySchemeDenied { .. }
- | RelayTransportError::RelayDestinationDenied { .. } => {
- RuntimeError::Config(format!("{source} must use websocket relay urls allowed by the active Nostr policy, got `{trimmed}`"))
- }
- other => RuntimeError::Config(format!(
- "{source} contains invalid relay url `{trimmed}`: {other}"
- )),
- })
-}
-
-pub(crate) fn nostr_relay_url_policy_for_url(value: &str) -> RelayUrlPolicy {
- if value.trim_start().starts_with("ws://") {
- RelayUrlPolicy::Local
- } else {
- RelayUrlPolicy::Public
- }
-}
-
-fn resolve_env_file_path(args: &RuntimeInvocationArgs, env: &dyn Environment) -> Option<PathBuf> {
- args.env_file
- .clone()
- .or_else(|| env.var(ENV_CLI_FILE_PATH).map(PathBuf::from))
- .or_else(|| {
- let default_path = PathBuf::from(DEFAULT_ENV_PATH);
- default_path.exists().then_some(default_path)
- })
-}
-
-fn resolve_output_format(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<OutputFormat, RuntimeError> {
- if args.output_format.is_some() && (args.json || args.ndjson) {
- return Err(RuntimeError::Config(
- "flags --output, --json, and --ndjson cannot be used together".to_owned(),
- ));
- }
-
- match (args.output_format, args.json, args.ndjson) {
- (_, true, true) => {
- return Err(RuntimeError::Config(
- "flags --json and --ndjson cannot be used together".to_owned(),
- ));
- }
- (Some(format), false, false) => return Ok(format.as_output_format()),
- (None, true, false) => return Ok(OutputFormat::Json),
- (None, false, true) => return Ok(OutputFormat::Ndjson),
- (None, false, false) => {}
- (Some(_), true, false) | (Some(_), false, true) => unreachable!(),
- }
- if let Some(value) = env_value(env, env_file, &[ENV_CLI_OUTPUT_FORMAT]) {
- return parse_output_format(value.as_str());
- }
-
- if let Some(value) = user_config
- .and_then(|config| config.output.as_ref())
- .and_then(|output| output.format.as_deref())
- {
- return parse_output_format(value);
- }
-
- match workspace_config
- .and_then(|config| config.output.as_ref())
- .and_then(|output| output.format.as_deref())
- {
- Some(value) => parse_output_format(value),
- None => Ok(OutputFormat::Terminal),
- }
-}
-
-fn resolve_verbosity(args: &RuntimeInvocationArgs) -> Result<Verbosity, RuntimeError> {
- let selected = [args.quiet, args.verbose, args.trace]
- .into_iter()
- .filter(|selected| *selected)
- .count();
- if selected > 1 {
- return Err(RuntimeError::Config(
- "flags --quiet, --verbose, and --trace are mutually exclusive".to_owned(),
- ));
- }
-
- if args.quiet {
- Ok(Verbosity::Quiet)
- } else if args.trace {
- Ok(Verbosity::Trace)
- } else if args.verbose {
- Ok(Verbosity::Verbose)
- } else {
- Ok(Verbosity::Normal)
- }
-}
-
-fn resolve_interaction_config(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
-) -> InteractionConfig {
- let stdin_tty = env.stdin_is_tty();
- let stdout_tty = env.stdout_is_tty();
- let input_enabled = !args.no_input;
- let prompts_allowed = input_enabled && stdin_tty && stdout_tty;
- let confirmations_allowed = prompts_allowed && !args.yes;
- InteractionConfig {
- input_enabled,
- assume_yes: args.yes,
- stdin_tty,
- stdout_tty,
- prompts_allowed,
- confirmations_allowed,
- }
-}
-
-fn resolve_logging_filter(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> String {
- args.log_filter
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_FILTER]))
- .or_else(|| {
- user_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.filter.clone())
- })
- .or_else(|| {
- workspace_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.filter.clone())
- })
- .unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned())
-}
-
-fn resolve_logging_directory(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
- default_logs_root: &Path,
-) -> Option<PathBuf> {
- args.log_dir
- .clone()
- .or_else(|| env_value(env, env_file, &[ENV_CLI_LOG_DIR]).map(PathBuf::from))
- .or_else(|| {
- user_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.output_dir.clone())
- })
- .or_else(|| {
- workspace_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.output_dir.clone())
- })
- .or_else(|| Some(default_logs_root.to_path_buf()))
-}
-
-fn resolve_logging_stdout(
- args: &RuntimeInvocationArgs,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<bool, RuntimeError> {
- resolve_bool_pair(
- args.log_stdout,
- args.no_log_stdout,
- &[ENV_CLI_LOG_STDOUT],
- user_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.stdout),
- workspace_config
- .and_then(|config| config.logging.as_ref())
- .and_then(|logging| logging.stdout),
- false,
- env,
- env_file,
- "--log-stdout",
- "--no-log-stdout",
- )
-}
-
-fn validate_logging_output_contract(
- output: &OutputConfig,
- logging: &LoggingConfig,
-) -> Result<(), RuntimeError> {
- if logging.stdout {
- return Err(RuntimeError::Config(format!(
- "stdout logging cannot be used with {} output; unset {ENV_CLI_LOG_STDOUT} or use --no-log-stdout",
- output.format.as_str()
- )));
- }
-
- Ok(())
-}
-
-#[expect(
- clippy::too_many_arguments,
- reason = "configuration precedence keeps each source explicit"
-)]
-fn resolve_bool_pair(
- positive_flag: bool,
- negative_flag: bool,
- env_keys: &[&str],
- user_value: Option<bool>,
- workspace_value: Option<bool>,
- default: bool,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- positive_label: &str,
- negative_label: &str,
-) -> Result<bool, RuntimeError> {
- match (positive_flag, negative_flag) {
- (true, true) => Err(RuntimeError::Config(format!(
- "flags {positive_label} and {negative_label} cannot be used together"
- ))),
- (true, false) => Ok(true),
- (false, true) => Ok(false),
- (false, false) => match env_value_entry(env, env_file, env_keys) {
- Some((key, value)) => parse_bool_env(key.as_str(), value.as_str()),
- None => Ok(user_value.or(workspace_value).unwrap_or(default)),
- },
- }
-}
-
-fn env_value(env: &dyn Environment, env_file: &EnvFileValues, keys: &[&str]) -> Option<String> {
- env_value_entry(env, env_file, keys).map(|(_, value)| value)
-}
-
-fn env_value_entry(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- keys: &[&str],
-) -> Option<(String, String)> {
- keys.iter()
- .find_map(|key| env.var(key).map(|value| ((*key).to_owned(), value)))
- .or_else(|| {
- keys.iter().find_map(|key| {
- env_file
- .0
- .get(*key)
- .cloned()
- .map(|value| ((*key).to_owned(), value))
- })
- })
-}
-
-fn load_env_file_values(path: Option<&Path>) -> Result<EnvFileValues, RuntimeError> {
- let Some(path) = path else {
- return Ok(EnvFileValues::default());
- };
- let raw = fs::read_to_string(path).map_err(|err| {
- RuntimeError::Config(format!("failed to read env file {}: {err}", path.display()))
- })?;
- parse_env_file_values(&raw, path)
-}
-
-fn parse_env_file_values(raw: &str, path: &Path) -> Result<EnvFileValues, RuntimeError> {
- parse_strict_env_file(raw, path, SUPPORTED_ENV_FILE_KEYS)
- .map(|values| EnvFileValues(values.into_inner()))
- .map_err(|err| RuntimeError::Config(err.to_string()))
-}
-
-fn parse_output_format(value: &str) -> Result<OutputFormat, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "terminal" => Ok(OutputFormat::Terminal),
- "json" => Ok(OutputFormat::Json),
- "ndjson" => Ok(OutputFormat::Ndjson),
- other => Err(RuntimeError::Config(format!(
- "{ENV_CLI_OUTPUT_FORMAT} must be `terminal`, `json`, or `ndjson`, got `{other}`"
- ))),
- }
-}
-
-fn parse_signer_mode(source: &str, value: String) -> Result<SignerBackend, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "local" => Ok(SignerBackend::Local),
- "myc" => Ok(SignerBackend::Myc),
- other => Err(RuntimeError::Config(format!(
- "{source} must be `local` or `myc`, got `{other}`"
- ))),
- }
-}
-
-fn parse_transport_profile_kind(
- source: &str,
- value: &str,
-) -> Result<TransportProfileKind, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "local_only" => Ok(TransportProfileKind::LocalOnly),
- "nostr" => Ok(TransportProfileKind::Nostr),
- "reticulum" => Ok(TransportProfileKind::Reticulum),
- "multi_target" => Ok(TransportProfileKind::MultiTarget),
- other => Err(RuntimeError::Config(format!(
- "{source} must be `local_only`, `nostr`, `reticulum`, or `multi_target`, got `{other}`"
- ))),
- }
-}
-
-fn parse_reticulum_behavior(source: &str, value: &str) -> Result<ReticulumBehavior, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "reject_delivery_attempts" => Ok(ReticulumBehavior::RejectDeliveryAttempts),
- "defer_delivery_plans" => Ok(ReticulumBehavior::DeferDeliveryPlans),
- other => Err(RuntimeError::Config(format!(
- "{source} must be `reject_delivery_attempts` or `defer_delivery_plans`, got `{other}`"
- ))),
- }
-}
-
-fn parse_reticulum_scope(source: &str, value: &str) -> Result<String, RuntimeError> {
- RadrootsTransportMeshScopeId::parse(value)
- .map(|scope| scope.as_str().to_owned())
- .map_err(|error| RuntimeError::Config(format!("{source} is invalid: {error}")))
-}
-
-fn parse_reticulum_agent_endpoint(source: &str, value: &str) -> Result<String, RuntimeError> {
- if value.is_empty()
- || value != value.trim()
- || value
- .chars()
- .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace())
- || value.find(':').is_none()
- {
- return Err(RuntimeError::Config(format!(
- "{source} must be a non-empty URI-like endpoint without whitespace"
- )));
- }
- Ok(value.to_owned())
-}
-
-fn resolve_account_secret_backend(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- user_config: Option<&CliConfigFile>,
- workspace_config: Option<&CliConfigFile>,
-) -> Result<Option<RadrootsSecretBackend>, RuntimeError> {
- if let Some((key, value)) = env_value_entry(env, env_file, &[ENV_CLI_ACCOUNT_SECRET_BACKEND]) {
- return parse_account_secret_backend(key.as_str(), value.as_str()).map(Some);
- }
-
- if let Some(value) = user_config
- .and_then(|config| config.account.as_ref())
- .and_then(|account| account.secret.as_ref())
- .and_then(|secret| secret.backend.as_deref())
- {
- return parse_account_secret_backend("user config [account.secret].backend", value)
- .map(Some);
- }
-
- workspace_config
- .and_then(|config| config.account.as_ref())
- .and_then(|account| account.secret.as_ref())
- .and_then(|secret| secret.backend.as_deref())
- .map(|value| {
- parse_account_secret_backend("workspace config [account.secret].backend", value)
- })
- .transpose()
-}
-
-fn parse_account_secret_backend(
- key: &str,
- value: &str,
-) -> Result<RadrootsSecretBackend, RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- "host_vault" => Ok(RadrootsSecretBackend::HostVault(
- RadrootsHostVaultPolicy::desktop(),
- )),
- "encrypted_file" => Ok(RadrootsSecretBackend::EncryptedFile),
- other => Err(RuntimeError::Config(format!(
- "{key} must be `host_vault` or `encrypted_file`, got `{other}`"
- ))),
- }
-}
-
-fn parse_bool_env(key: &str, value: &str) -> Result<bool, RuntimeError> {
- parse_bool_value(key, value).map_err(|err| RuntimeError::Config(err.to_string()))
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
- CapabilityBindingSource, CapabilityBindingTargetKind, DEFAULT_HYF_EXECUTABLE,
- DEFAULT_LOG_FILTER, DEFAULT_MYC_STATUS_TIMEOUT_MS, DEFAULT_RPC_URL, EnvFileValues,
- Environment, HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, InteractionConfig, OutputConfig,
- OutputFormat, PathsConfig, RuntimeConfig, SignerBackend, TransportConfigSource,
- TransportProfileKind, Verbosity, parse_env_file_values,
- };
- use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
- use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
- use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
- use std::collections::BTreeMap;
- use std::fs;
- use std::path::{Path, PathBuf};
- use tempfile::tempdir;
-
- struct MapEnvironment {
- values: BTreeMap<String, String>,
- current_dir: PathBuf,
- path_resolver: RadrootsPathResolver,
- stdin_tty: bool,
- stdout_tty: bool,
- }
-
- impl MapEnvironment {
- fn new(values: BTreeMap<String, String>) -> Self {
- Self {
- values,
- current_dir: PathBuf::from("/workspaces/radroots-cli"),
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/tester")),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- }
- }
-
- fn with_tty(mut self, stdin_tty: bool, stdout_tty: bool) -> Self {
- self.stdin_tty = stdin_tty;
- self.stdout_tty = stdout_tty;
- self
- }
- }
-
- impl Environment for MapEnvironment {
- fn var(&self, key: &str) -> Option<String> {
- self.values.get(key).cloned()
- }
-
- fn current_dir(&self) -> Result<PathBuf, crate::runtime::RuntimeError> {
- Ok(self.current_dir.clone())
- }
-
- fn path_resolver(&self) -> RadrootsPathResolver {
- self.path_resolver.clone()
- }
-
- fn stdin_is_tty(&self) -> bool {
- self.stdin_tty
- }
-
- fn stdout_is_tty(&self) -> bool {
- self.stdout_tty
- }
- }
-
- fn repo_local_env(
- workspace_root: PathBuf,
- repo_local_root: PathBuf,
- user_home: PathBuf,
- mut values: BTreeMap<String, String>,
- ) -> MapEnvironment {
- values.insert(
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- );
- values.insert(
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- );
-
- MapEnvironment {
- values,
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- }
- }
-
- fn runtime_args() -> RuntimeInvocationArgs {
- RuntimeInvocationArgs::default()
- }
-
- #[test]
- fn flags_override_environment_values() {
- let args = RuntimeInvocationArgs {
- output_format: Some(RuntimeOutputFormatArg::Terminal),
- verbose: true,
- dry_run: true,
- log_filter: Some("debug".to_owned()),
- log_stdout: false,
- identity_path: Some(PathBuf::from("custom-identity.json")),
- signer: Some("local".to_owned()),
- myc_executable: Some(PathBuf::from("bin/myc-cli")),
- myc_status_timeout_ms: Some(2500),
- hyf_enabled: true,
- hyf_executable: Some(PathBuf::from("bin/hyfd-cli")),
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::from([
- (
- "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
- "terminal".to_owned(),
- ),
- ("RADROOTS_CLI_LOGGING_FILTER".to_owned(), "trace".to_owned()),
- ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "false".to_owned()),
- (
- "RADROOTS_CLI_IDENTITY_PATH".to_owned(),
- "env-identity.json".to_owned(),
- ),
- ("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
- (
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "nostr".to_owned(),
- ),
- (
- "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
- "wss://relay.one,wss://relay.two".to_owned(),
- ),
- (
- "RADROOTS_CLI_MYC_EXECUTABLE".to_owned(),
- "env-myc".to_owned(),
- ),
- (
- "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
- "9000".to_owned(),
- ),
- ("RADROOTS_CLI_HYF_ENABLED".to_owned(), "false".to_owned()),
- (
- "RADROOTS_CLI_HYF_EXECUTABLE".to_owned(),
- "env-hyfd".to_owned(),
- ),
- ]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
- assert_eq!(
- resolved.output,
- OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Verbose,
- dry_run: true,
- }
- );
- assert_eq!(
- resolved.interaction,
- InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- }
- );
- assert_eq!(
- resolved.paths,
- PathsConfig {
- profile: "interactive_user".to_owned(),
- profile_source: "default".to_owned(),
- allowed_profiles: vec!["interactive_user".to_owned(), "repo_local".to_owned(),],
- root_source: "host_defaults".to_owned(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".to_owned(),
- app_namespace: "apps/cli".to_owned(),
- shared_accounts_namespace: "shared/accounts".to_owned(),
- shared_identities_namespace: "shared/identities".to_owned(),
- app_config_path: PathBuf::from(
- "/home/tester/.radroots/config/apps/cli/config.toml"
- ),
- workspace_config_path: None,
- app_data_root: PathBuf::from("/home/tester/.radroots/data/apps/cli"),
- shared_cache_root: PathBuf::from("/home/tester/.radroots/cache"),
- app_logs_root: PathBuf::from("/home/tester/.radroots/logs/apps/cli"),
- shared_accounts_data_root: PathBuf::from(
- "/home/tester/.radroots/data/shared/accounts"
- ),
- shared_accounts_secrets_root: PathBuf::from(
- "/home/tester/.radroots/secrets/shared/accounts"
- ),
- default_identity_path: PathBuf::from(
- "/home/tester/.radroots/secrets/shared/identities/default.json"
- ),
- }
- );
- assert_eq!(resolved.logging.filter, "debug");
- assert!(!resolved.logging.stdout);
- assert_eq!(
- resolved.identity.path,
- PathBuf::from("custom-identity.json")
- );
- assert_eq!(
- resolved.account,
- AccountConfig {
- selector: None,
- store_path: PathBuf::from("/home/tester/.radroots/data/shared/accounts/store.json"),
- secrets_dir: PathBuf::from("/home/tester/.radroots/secrets/shared/accounts"),
- secret_backend: RadrootsSecretBackend::HostVault(
- RadrootsHostVaultPolicy::desktop(),
- ),
- }
- );
- assert_eq!(
- resolved.account_secret_contract,
- AccountSecretContractConfig {
- default_backend: "host_vault".to_owned(),
- allowed_backends: vec!["host_vault".to_owned(), "encrypted_file".to_owned(),],
- host_vault_policy: Some("desktop".to_owned()),
- uses_protected_store: true,
- }
- );
- assert_eq!(resolved.signer.backend, SignerBackend::Local);
- assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(
- resolved.transport.nostr_relay_urls,
- vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
- );
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc-cli"));
- assert_eq!(resolved.myc.status_timeout_ms, 2500);
- assert_eq!(
- resolved.hyf,
- HyfConfig {
- enabled: true,
- executable: PathBuf::from("bin/hyfd-cli"),
- }
- );
- }
-
- #[test]
- fn environment_values_fill_missing_flags() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_CLI_OUTPUT_FORMAT".to_owned(), "json".to_owned()),
- (
- "RADROOTS_CLI_LOGGING_FILTER".to_owned(),
- "debug,cli=trace".to_owned(),
- ),
- (
- "RADROOTS_CLI_LOGGING_OUTPUT_DIR".to_owned(),
- "logs/runtime".to_owned(),
- ),
- ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "false".to_owned()),
- (
- "RADROOTS_CLI_ACCOUNT_SELECTOR".to_owned(),
- "acct_demo".to_owned(),
- ),
- (
- "RADROOTS_CLI_IDENTITY_PATH".to_owned(),
- "state/identity.json".to_owned(),
- ),
- ("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned()),
- (
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "nostr".to_owned(),
- ),
- (
- "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
- "wss://relay.one,wss://relay.two".to_owned(),
- ),
- (
- "RADROOTS_CLI_MYC_EXECUTABLE".to_owned(),
- "bin/myc".to_owned(),
- ),
- (
- "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
- "3500".to_owned(),
- ),
- ("RADROOTS_CLI_HYF_ENABLED".to_owned(), "true".to_owned()),
- (
- "RADROOTS_CLI_HYF_EXECUTABLE".to_owned(),
- "bin/hyfd".to_owned(),
- ),
- ]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
- assert_eq!(
- resolved.output,
- OutputConfig {
- format: OutputFormat::Json,
- verbosity: Verbosity::Normal,
- dry_run: false,
- }
- );
- assert_eq!(
- resolved.interaction,
- InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: false,
- stdout_tty: false,
- prompts_allowed: false,
- confirmations_allowed: false,
- }
- );
- assert_eq!(resolved.logging.filter, "debug,cli=trace");
- assert_eq!(
- resolved.logging.directory,
- Some(PathBuf::from("logs/runtime"))
- );
- assert!(!resolved.logging.stdout);
- assert_eq!(resolved.account.selector.as_deref(), Some("acct_demo"));
- assert_eq!(
- resolved.account.secret_backend,
- RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop())
- );
- assert_eq!(resolved.identity.path, PathBuf::from("state/identity.json"));
- assert_eq!(resolved.signer.backend, SignerBackend::Myc);
- assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(
- resolved.transport.nostr_relay_urls,
- vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
- );
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc"));
- assert_eq!(resolved.myc.status_timeout_ms, 3500);
- assert_eq!(
- resolved.hyf,
- HyfConfig {
- enabled: true,
- executable: PathBuf::from("bin/hyfd"),
- }
- );
- }
-
- #[test]
- fn old_process_environment_names_have_no_effect() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_OUTPUT".to_owned(), "json".to_owned()),
- ("RADROOTS_LOG_FILTER".to_owned(), "trace".to_owned()),
- ("RADROOTS_LOG_DIR".to_owned(), "logs/old".to_owned()),
- ("RADROOTS_LOG_STDOUT".to_owned(), "true".to_owned()),
- ("RADROOTS_ACCOUNT".to_owned(), "old_account".to_owned()),
- (
- "RADROOTS_ACCOUNT_SECRET_BACKEND".to_owned(),
- "encrypted_file".to_owned(),
- ),
- (
- "RADROOTS_IDENTITY_PATH".to_owned(),
- "old-identity.json".to_owned(),
- ),
- ("RADROOTS_SIGNER".to_owned(), "myc".to_owned()),
- ("RADROOTS_PUBLISH_MODE".to_owned(), "legacy".to_owned()),
- (
- "RADROOTS_RELAYS".to_owned(),
- "wss://old-relay.example".to_owned(),
- ),
- ("RADROOTS_MYC_EXECUTABLE".to_owned(), "old-myc".to_owned()),
- (
- "RADROOTS_MYC_STATUS_TIMEOUT_MS".to_owned(),
- "9999".to_owned(),
- ),
- ("RADROOTS_HYF_ENABLED".to_owned(), "true".to_owned()),
- ("RADROOTS_HYF_EXECUTABLE".to_owned(), "old-hyfd".to_owned()),
- (
- "RADROOTS_RPC_URL".to_owned(),
- "http://127.0.0.1:9".to_owned(),
- ),
- (
- "RADROOTS_RPC_BEARER_TOKEN".to_owned(),
- "old-token".to_owned(),
- ),
- (
- "RADROOTS_TRUSTED_RHI_WORKER_PUBKEYS".to_owned(),
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_owned(),
- ),
- ]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
-
- assert_eq!(resolved.output.format, OutputFormat::Terminal);
- assert_eq!(resolved.logging.filter, DEFAULT_LOG_FILTER);
- assert!(!resolved.logging.stdout);
- assert_eq!(resolved.account.selector, None);
- assert_eq!(
- resolved.account.secret_backend,
- RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop())
- );
- assert_eq!(resolved.signer.backend, SignerBackend::Local);
- assert_eq!(resolved.transport.profile, TransportProfileKind::LocalOnly);
- assert_eq!(resolved.transport.nostr_relay_urls, Vec::<String>::new());
- assert_eq!(resolved.myc.executable, PathBuf::from("myc"));
- assert_eq!(
- resolved.myc.status_timeout_ms,
- DEFAULT_MYC_STATUS_TIMEOUT_MS
- );
- assert!(!resolved.hyf.enabled);
- assert_eq!(
- resolved.hyf.executable,
- PathBuf::from(DEFAULT_HYF_EXECUTABLE)
- );
- assert_eq!(resolved.rpc.url, DEFAULT_RPC_URL);
- assert_eq!(resolved.rhi.validator_set, None);
- assert!(!resolved.rhi.require_cryptographic_proof);
- }
-
- #[test]
- fn toml_output_logging_account_and_identity_config_resolve() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- app_config_dir.join("config.toml"),
- r#"
-[output]
-format = "json"
-
-[logging]
-filter = "debug,cli=trace"
-output_dir = "logs/from-toml"
-stdout = false
-
-[account]
-selector = "acct_from_toml"
-
-[account.secret]
-backend = "encrypted_file"
-
-[identity]
-path = "identity/from-toml.json"
-"#,
- )
- .expect("write user config");
-
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve toml config");
-
- assert_eq!(resolved.output.format, OutputFormat::Json);
- assert_eq!(resolved.logging.filter, "debug,cli=trace");
- assert_eq!(
- resolved.logging.directory,
- Some(PathBuf::from("logs/from-toml"))
- );
- assert!(!resolved.logging.stdout);
- assert_eq!(resolved.account.selector.as_deref(), Some("acct_from_toml"));
- assert_eq!(
- resolved.account.secret_backend,
- RadrootsSecretBackend::EncryptedFile
- );
- assert_eq!(
- resolved.identity.path,
- PathBuf::from("identity/from-toml.json")
- );
- }
-
- #[test]
- fn conflicting_boolean_flags_fail() {
- let args = RuntimeInvocationArgs {
- log_stdout: true,
- no_log_stdout: true,
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::new());
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("conflicting flags");
- assert!(error.to_string().contains("cannot be used together"));
-
- let hyf_args = RuntimeInvocationArgs {
- hyf_enabled: true,
- no_hyf_enabled: true,
- ..runtime_args()
- };
- let error =
- RuntimeConfig::resolve_with_env_file(&hyf_args, &env, &EnvFileValues::default())
- .expect_err("conflicting hyf flags");
- assert!(error.to_string().contains("--hyf-enabled"));
- }
-
- #[test]
- fn conflicting_output_and_verbosity_flags_fail() {
- let env = MapEnvironment::new(BTreeMap::new());
-
- let conflicting_output = RuntimeInvocationArgs {
- json: true,
- ndjson: true,
- ..runtime_args()
- };
- let error = RuntimeConfig::resolve_with_env_file(
- &conflicting_output,
- &env,
- &EnvFileValues::default(),
- )
- .expect_err("conflicting output flags");
- assert!(error.to_string().contains("--json and --ndjson"));
-
- let conflicting_verbosity = RuntimeInvocationArgs {
- quiet: true,
- trace: true,
- ..runtime_args()
- };
- let error = RuntimeConfig::resolve_with_env_file(
- &conflicting_verbosity,
- &env,
- &EnvFileValues::default(),
- )
- .expect_err("conflicting verbosity flags");
- assert!(
- error
- .to_string()
- .contains("--quiet, --verbose, and --trace")
- );
-
- let conflicting_aliases = RuntimeInvocationArgs {
- output_format: Some(RuntimeOutputFormatArg::Json),
- json: true,
- ..runtime_args()
- };
- let error = RuntimeConfig::resolve_with_env_file(
- &conflicting_aliases,
- &env,
- &EnvFileValues::default(),
- )
- .expect_err("conflicting output aliases");
- assert!(error.to_string().contains("--output, --json, and --ndjson"));
- }
-
- #[test]
- fn output_rejects_stdout_logging_flags() {
- let env = MapEnvironment::new(BTreeMap::new());
-
- let terminal_args = RuntimeInvocationArgs {
- output_format: Some(RuntimeOutputFormatArg::Terminal),
- log_stdout: true,
- ..runtime_args()
- };
- let error =
- RuntimeConfig::resolve_with_env_file(&terminal_args, &env, &EnvFileValues::default())
- .expect_err("terminal stdout logging should fail");
- let message = error.to_string();
- assert!(message.contains("stdout logging"));
- assert!(message.contains("terminal output"));
- assert!(message.contains("--no-log-stdout"));
-
- let json_args = RuntimeInvocationArgs {
- json: true,
- log_stdout: true,
- ..runtime_args()
- };
- let error =
- RuntimeConfig::resolve_with_env_file(&json_args, &env, &EnvFileValues::default())
- .expect_err("json stdout logging should fail");
- let message = error.to_string();
- assert!(message.contains("stdout logging"));
- assert!(message.contains("json output"));
- assert!(message.contains("--no-log-stdout"));
-
- let ndjson_args = RuntimeInvocationArgs {
- ndjson: true,
- log_stdout: true,
- ..runtime_args()
- };
- let error =
- RuntimeConfig::resolve_with_env_file(&ndjson_args, &env, &EnvFileValues::default())
- .expect_err("ndjson stdout logging should fail");
- let message = error.to_string();
- assert!(message.contains("stdout logging"));
- assert!(message.contains("ndjson output"));
- }
-
- #[test]
- fn output_rejects_stdout_logging_environment() {
- let terminal_args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
- "true".to_owned(),
- )]));
- let error =
- RuntimeConfig::resolve_with_env_file(&terminal_args, &env, &EnvFileValues::default())
- .expect_err("terminal stdout logging from env should fail");
- assert!(error.to_string().contains("terminal output"));
-
- let json_args = RuntimeInvocationArgs {
- json: true,
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
- "true".to_owned(),
- )]));
- let error =
- RuntimeConfig::resolve_with_env_file(&json_args, &env, &EnvFileValues::default())
- .expect_err("json stdout logging from env should fail");
- let message = error.to_string();
- assert!(message.contains("RADROOTS_CLI_LOGGING_STDOUT"));
- assert!(message.contains("RADROOTS_CLI_LOGGING_STDOUT"));
-
- let ndjson_env_args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_CLI_OUTPUT_FORMAT".to_owned(), "ndjson".to_owned()),
- ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "true".to_owned()),
- ]));
- let error =
- RuntimeConfig::resolve_with_env_file(&ndjson_env_args, &env, &EnvFileValues::default())
- .expect_err("ndjson stdout logging from env should fail");
- assert!(error.to_string().contains("ndjson output"));
- }
-
- #[test]
- fn no_log_stdout_overrides_environment_for_output() {
- let args = RuntimeInvocationArgs {
- json: true,
- no_log_stdout: true,
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
- "true".to_owned(),
- )]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve output with stdout logging disabled");
- assert_eq!(resolved.output.format, OutputFormat::Json);
- assert!(!resolved.logging.stdout);
- }
-
- #[test]
- fn invalid_environment_value_fails() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_LOGGING_STDOUT".to_owned(),
- "maybe".to_owned(),
- )]));
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid bool");
- assert!(error.to_string().contains("RADROOTS_CLI_LOGGING_STDOUT"));
-
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS".to_owned(),
- "slow".to_owned(),
- )]));
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid myc timeout");
- assert!(
- error
- .to_string()
- .contains("RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS")
- );
-
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "relay".to_owned(),
- )]));
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid transport profile");
- assert!(error.to_string().contains("RADROOTS_CLI_TRANSPORT_PROFILE"));
- assert!(error.to_string().contains("local_only"));
- assert!(error.to_string().contains("reticulum"));
-
- let args = RuntimeInvocationArgs {
- myc_status_timeout_ms: Some(0),
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::new());
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("zero myc timeout");
- assert!(error.to_string().contains("greater than zero"));
- }
-
- #[test]
- fn env_file_values_fill_missing_flags() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::new());
- let env_file = parse_env_file_values(
- r#"
-RADROOTS_CLI_OUTPUT_FORMAT=json
-RADROOTS_CLI_LOGGING_FILTER="debug,radroots_cli=trace"
-RADROOTS_CLI_LOGGING_OUTPUT_DIR=/tmp/radroots-cli-logs
-RADROOTS_CLI_LOGGING_STDOUT=false
-RADROOTS_CLI_ACCOUNT_SELECTOR=acct_env_file
-RADROOTS_CLI_IDENTITY_PATH=state/identity.json
-RADROOTS_CLI_SIGNER_BACKEND=myc
-RADROOTS_CLI_TRANSPORT_PROFILE=nostr
-RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS=wss://relay.env-file
-RADROOTS_CLI_MYC_EXECUTABLE=bin/myc
-RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS=4500
-RADROOTS_CLI_HYF_ENABLED=true
-RADROOTS_CLI_HYF_EXECUTABLE=bin/hyfd
-"#,
- Path::new(".env.test"),
- )
- .expect("parse env file");
-
- let resolved =
- RuntimeConfig::resolve_with_env_file(&args, &env, &env_file).expect("resolve config");
- assert_eq!(resolved.output.format, OutputFormat::Json);
- assert_eq!(resolved.logging.filter, "debug,radroots_cli=trace");
- assert_eq!(
- resolved.logging.directory,
- Some(PathBuf::from("/tmp/radroots-cli-logs"))
- );
- assert!(!resolved.logging.stdout);
- assert_eq!(resolved.account.selector.as_deref(), Some("acct_env_file"));
- assert_eq!(resolved.identity.path, PathBuf::from("state/identity.json"));
- assert_eq!(resolved.signer.backend, SignerBackend::Myc);
- assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(
- resolved.transport.nostr_relay_urls,
- vec!["wss://relay.env-file".to_owned()]
- );
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- assert_eq!(resolved.myc.executable, PathBuf::from("bin/myc"));
- assert_eq!(resolved.myc.status_timeout_ms, 4500);
- assert_eq!(
- resolved.hyf,
- HyfConfig {
- enabled: true,
- executable: PathBuf::from("bin/hyfd"),
- }
- );
- }
-
- #[test]
- fn explicit_output_flag_overrides_environment_output() {
- let args = RuntimeInvocationArgs {
- output_format: Some(RuntimeOutputFormatArg::Ndjson),
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
- "json".to_owned(),
- )]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
- assert_eq!(resolved.output.format, OutputFormat::Ndjson);
- }
-
- #[test]
- fn interaction_config_reflects_tty_and_flags() {
- let args = RuntimeInvocationArgs {
- no_input: true,
- yes: true,
- ..runtime_args()
- };
- let env = MapEnvironment::new(BTreeMap::new()).with_tty(true, true);
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
- assert_eq!(
- resolved.interaction,
- InteractionConfig {
- input_enabled: false,
- assume_yes: true,
- stdin_tty: true,
- stdout_tty: true,
- prompts_allowed: false,
- confirmations_allowed: false,
- }
- );
-
- let interactive_args = runtime_args();
- let interactive = RuntimeConfig::resolve_with_env_file(
- &interactive_args,
- &env,
- &EnvFileValues::default(),
- )
- .expect("resolve interactive runtime config");
- assert_eq!(
- interactive.interaction,
- InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: true,
- stdout_tty: true,
- prompts_allowed: true,
- confirmations_allowed: true,
- }
- );
- }
-
- #[test]
- fn process_environment_overrides_env_file_values() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- ("RADROOTS_CLI_LOGGING_FILTER".to_owned(), "info".to_owned()),
- ("RADROOTS_CLI_LOGGING_STDOUT".to_owned(), "false".to_owned()),
- ]));
- let env_file = parse_env_file_values(
- r#"
-RADROOTS_CLI_LOGGING_FILTER=debug
-RADROOTS_CLI_LOGGING_STDOUT=true
-"#,
- Path::new(".env.test"),
- )
- .expect("parse env file");
-
- let resolved =
- RuntimeConfig::resolve_with_env_file(&args, &env, &env_file).expect("resolve config");
- assert_eq!(resolved.output.format, OutputFormat::Terminal);
- assert_eq!(resolved.logging.filter, "info");
- assert!(!resolved.logging.stdout);
- }
-
- #[test]
- fn user_transport_config_overrides_workspace_transport_config() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.workspace\"]\n",
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\", \"wss://relay.workspace\"]\n",
- )
- .expect("write user config");
-
- let env = MapEnvironment {
- values: BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- ),
- ]),
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
- assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
- assert_eq!(
- resolved.transport.nostr_relay_urls,
- vec![
- "wss://relay.user".to_owned(),
- "wss://relay.workspace".to_owned()
- ]
- );
- assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
- }
-
- #[test]
- fn multi_target_transport_config_uses_nostr_targets_and_reticulum_behavior() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- app_config_dir.join("config.toml"),
- "[transport]\nprofile = \"multi_target\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\", \"wss://relay.backup\"]\n\n[transport.reticulum]\nbehavior = \"defer_delivery_plans\"\nscope = \"farmers_market\"\nagent_endpoint = \"reticulum-agent:local\"\n",
- )
- .expect("write user config");
-
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve multi-target transport profile");
-
- assert_eq!(
- resolved.transport.profile,
- TransportProfileKind::MultiTarget
- );
- assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
- assert_eq!(
- resolved.transport.nostr_relay_urls,
- vec![
- "wss://relay.user".to_owned(),
- "wss://relay.backup".to_owned()
- ]
- );
- assert_eq!(
- resolved.transport.reticulum_behavior,
- super::ReticulumBehavior::DeferDeliveryPlans
- );
- assert_eq!(resolved.transport.reticulum_scope, "farmers_market");
- assert_eq!(
- resolved.transport.reticulum_agent_endpoint.as_deref(),
- Some("reticulum-agent:local")
- );
- }
-
- #[test]
- fn transport_profile_precedence_tracks_source() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[transport]\nprofile = \"reticulum\"\n",
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- "[transport]\nprofile = \"nostr\"\n\n[transport.nostr]\nrelay_urls = [\"wss://relay.user\"]\n",
- )
- .expect("write user config");
-
- let env = repo_local_env(
- workspace_root.clone(),
- repo_local_root.clone(),
- user_home.clone(),
- BTreeMap::from([(
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "reticulum".to_owned(),
- )]),
- );
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve environment transport profile");
- assert_eq!(resolved.transport.profile, TransportProfileKind::Reticulum);
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::Environment
- );
- let env = repo_local_env(
- workspace_root.clone(),
- repo_local_root.clone(),
- user_home.clone(),
- BTreeMap::new(),
- );
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve user transport profile");
- assert_eq!(resolved.transport.profile, TransportProfileKind::Nostr);
- assert_eq!(resolved.transport.source, TransportConfigSource::UserConfig);
- fs::remove_file(app_config_dir.join("config.toml")).expect("remove user config");
- let env = repo_local_env(
- workspace_root.clone(),
- repo_local_root.clone(),
- user_home.clone(),
- BTreeMap::new(),
- );
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve workspace transport profile");
- assert_eq!(resolved.transport.profile, TransportProfileKind::Reticulum);
- assert_eq!(
- resolved.transport.source,
- TransportConfigSource::WorkspaceConfig
- );
- fs::remove_file(repo_local_root.join("config.toml")).expect("remove workspace config");
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let resolved =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect("resolve default transport profile");
- assert_eq!(resolved.transport.profile, TransportProfileKind::LocalOnly);
- assert_eq!(resolved.transport.source, TransportConfigSource::Defaults);
- }
-
- #[test]
- fn user_hyf_config_overrides_workspace_hyf_config() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[hyf]\nenabled = false\nexecutable = \"workspace-hyfd\"\n",
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- "[hyf]\nenabled = true\nexecutable = \"user-hyfd\"\n",
- )
- .expect("write user config");
-
- let env = MapEnvironment {
- values: BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- ),
- ]),
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(
- resolved.hyf,
- HyfConfig {
- enabled: true,
- executable: PathBuf::from("user-hyfd"),
- }
- );
- }
-
- #[test]
- fn user_myc_config_overrides_workspace_myc_config() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[myc]\nexecutable = \"workspace-myc\"\nstatus_timeout_ms = 9000\n",
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- "[myc]\nexecutable = \"user-myc\"\nstatus_timeout_ms = 3000\n",
- )
- .expect("write user config");
-
- let env = MapEnvironment {
- values: BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- ),
- ]),
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(resolved.myc.executable, PathBuf::from("user-myc"));
- assert_eq!(resolved.myc.status_timeout_ms, 3000);
- }
-
- #[test]
- fn user_signer_config_overrides_workspace_signer_config() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[signer]\nbackend = \"myc\"\n",
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- "[signer]\nbackend = \"local\"\n",
- )
- .expect("write user config");
-
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(resolved.signer.backend, SignerBackend::Local);
- }
-
- #[test]
- fn environment_signer_overrides_user_signer_config() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- app_config_dir.join("config.toml"),
- "[signer]\nbackend = \"local\"\n",
- )
- .expect("write user config");
-
- let env = repo_local_env(
- workspace_root,
- repo_local_root,
- user_home,
- BTreeMap::from([("RADROOTS_CLI_SIGNER_BACKEND".to_owned(), "myc".to_owned())]),
- );
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(resolved.signer.backend, SignerBackend::Myc);
- }
-
- #[test]
- fn invalid_signer_config_reports_config_source() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[signer]\nbackend = \"remote\"\n",
- )
- .expect("write workspace config");
-
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let args = runtime_args();
-
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid signer mode");
- let message = error.to_string();
- assert!(message.contains("workspace config [signer].backend"));
- assert!(!message.contains("--signer"));
- }
-
- #[test]
- fn invalid_transport_config_reports_config_source() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- "[transport]\nprofile = \"relay\"\n",
- )
- .expect("write workspace config");
-
- let env = repo_local_env(workspace_root, repo_local_root, user_home, BTreeMap::new());
- let error =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect_err("invalid transport profile");
- let message = error.to_string();
- assert!(message.contains("workspace config [transport].profile"));
- assert!(message.contains("local_only"));
- assert!(message.contains("reticulum"));
- }
-
- #[test]
- fn user_capability_binding_overrides_workspace_binding() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let app_config_dir = repo_local_root.join("config/apps/cli");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(&app_config_dir).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "inference.hyf_stdio"
-provider = "hyf"
-target_kind = "managed_instance"
-target = "workspace-hyf"
-"#,
- )
- .expect("write workspace config");
- fs::write(
- app_config_dir.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "inference.hyf_stdio"
-provider = "hyf"
-target_kind = "explicit_endpoint"
-target = "bin/user-hyfd"
-"#,
- )
- .expect("write user config");
-
- let env = MapEnvironment {
- values: BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- ),
- ]),
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
- let args = runtime_args();
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve config");
- assert_eq!(resolved.capability_bindings.len(), 1);
- assert_eq!(
- resolved.capability_bindings[0],
- CapabilityBindingConfig {
- capability_id: INFERENCE_HYF_STDIO_CAPABILITY.to_owned(),
- provider_runtime_id: "hyf".to_owned(),
- binding_model: "stdio_service".to_owned(),
- target_kind: CapabilityBindingTargetKind::ExplicitEndpoint,
- target: "bin/user-hyfd".to_owned(),
- managed_account_ref: None,
- signer_session_ref: None,
- source: CapabilityBindingSource::UserConfig,
- }
- );
- }
-
- #[test]
- fn daemon_and_workflow_capability_bindings_are_rejected() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
- fs::create_dir_all(user_home.join(".radroots/config/apps/cli")).expect("app config dir");
- fs::write(
- repo_local_root.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "write_plane.trade_jsonrpc"
- provider = "legacy_provider"
-target_kind = "explicit_endpoint"
-target = "https://rpc.workspace.test/jsonrpc"
-"#,
- )
- .expect("write workspace config");
-
- let env = MapEnvironment {
- values: BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- repo_local_root.display().to_string(),
- ),
- ]),
- current_dir: workspace_root,
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(user_home),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
- let args = runtime_args();
-
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("rejected daemon capability binding");
- assert!(
- error
- .to_string()
- .contains("unknown capability_binding capability `write_plane.trade_jsonrpc`")
- );
-
- fs::write(
- repo_local_root.join("config.toml"),
- r#"
-[[capability_binding]]
-capability = "workflow.trade"
-provider = "rhi"
-target_kind = "managed_instance"
-target = "workflow-default"
-"#,
- )
- .expect("write workflow config");
-
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("rejected workflow capability binding");
- assert!(
- error
- .to_string()
- .contains("unknown capability_binding capability `workflow.trade`")
- );
- }
-
- #[test]
- fn invalid_relay_url_fails() {
- for relay in [
- "https://not-a-websocket.example.com",
- "wss://",
- "wss://user@relay.example",
- "wss://relay.example:abc",
- " ",
- ] {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- (
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "nostr".to_owned(),
- ),
- (
- "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
- relay.to_owned(),
- ),
- ]));
- let error =
- RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("invalid relay url");
- assert!(
- error.to_string().contains("relay url")
- || error.to_string().contains("websocket relay urls"),
- "unexpected error for {relay}: {error}"
- );
- }
- }
-
- #[test]
- fn relay_env_value_rejects_empty_entries() {
- let env = MapEnvironment::new(BTreeMap::from([
- (
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "nostr".to_owned(),
- ),
- (
- super::ENV_CLI_TRANSPORT_NOSTR_RELAY_URLS.to_owned(),
- "wss://relay.example,,wss://relay-two.example".to_owned(),
- ),
- ]));
- let error =
- RuntimeConfig::resolve_with_env_file(&runtime_args(), &env, &EnvFileValues::default())
- .expect_err("empty relay entry");
-
- assert!(error.to_string().contains("empty relay url"));
- }
-
- #[test]
- fn valid_ipv6_relay_url_resolves() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- (
- "RADROOTS_CLI_TRANSPORT_PROFILE".to_owned(),
- "nostr".to_owned(),
- ),
- (
- "RADROOTS_CLI_TRANSPORT_NOSTR_RELAY_URLS".to_owned(),
- " ws://[::1]:443/relay ".to_owned(),
- ),
- ]));
- let config = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("valid relay url");
-
- assert_eq!(
- config.transport.nostr_relay_urls,
- vec!["ws://[::1]:443/relay"]
- );
- }
-
- #[test]
- fn state_roots_are_resolved_from_home_and_workspace() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::new());
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
-
- assert_eq!(
- resolved.paths.app_config_path,
- PathBuf::from("/home/tester/.radroots/config/apps/cli/config.toml")
- );
- assert_eq!(resolved.paths.profile_source, "default");
- assert_eq!(resolved.paths.root_source, "host_defaults");
- assert_eq!(resolved.paths.repo_local_root, None);
- assert_eq!(resolved.paths.repo_local_root_source, None);
- assert_eq!(
- resolved.paths.subordinate_path_override_source,
- "runtime_config"
- );
- assert_eq!(resolved.paths.app_namespace, "apps/cli");
- assert_eq!(resolved.paths.shared_accounts_namespace, "shared/accounts");
- assert_eq!(
- resolved.paths.shared_identities_namespace,
- "shared/identities"
- );
- assert_eq!(resolved.paths.workspace_config_path, None);
- assert_eq!(
- resolved.paths.app_data_root,
- PathBuf::from("/home/tester/.radroots/data/apps/cli")
- );
- assert_eq!(
- resolved.paths.allowed_profiles,
- vec!["interactive_user".to_owned(), "repo_local".to_owned(),]
- );
- }
-
- #[test]
- fn windows_roots_use_native_user_directories() {
- let args = runtime_args();
- let env = MapEnvironment {
- values: BTreeMap::new(),
- current_dir: PathBuf::from(r"C:\workspaces\radroots-cli"),
- path_resolver: RadrootsPathResolver::new(
- RadrootsPlatform::Windows,
- RadrootsHostEnvironment {
- appdata_dir: Some(PathBuf::from(r"C:\Users\tester\AppData\Roaming")),
- localappdata_dir: Some(PathBuf::from(r"C:\Users\tester\AppData\Local")),
- ..RadrootsHostEnvironment::default()
- },
- ),
- stdin_tty: false,
- stdout_tty: false,
- };
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
-
- assert_eq!(
- resolved.paths.app_config_path,
- PathBuf::from(r"C:\Users\tester\AppData\Roaming")
- .join("Radroots")
- .join("config")
- .join("apps")
- .join("cli")
- .join("config.toml")
- );
- assert_eq!(
- resolved.paths.app_data_root,
- PathBuf::from(r"C:\Users\tester\AppData\Local")
- .join("Radroots")
- .join("data")
- .join("apps")
- .join("cli")
- );
- assert_eq!(
- resolved.paths.shared_accounts_data_root,
- PathBuf::from(r"C:\Users\tester\AppData\Local")
- .join("Radroots")
- .join("data")
- .join("shared")
- .join("accounts")
- );
- assert_eq!(
- resolved.paths.default_identity_path,
- PathBuf::from(r"C:\Users\tester\AppData\Roaming")
- .join("Radroots")
- .join("secrets")
- .join("shared")
- .join("identities")
- .join("default.json")
- );
- }
-
- #[test]
- fn repo_local_profile_uses_explicit_repo_local_root() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([
- (
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- ),
- (
- "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned(),
- ".local/radroots/dev".to_owned(),
- ),
- ]));
-
- let resolved = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
-
- assert_eq!(resolved.paths.profile, "repo_local");
- assert_eq!(
- resolved.paths.profile_source,
- "process_env:RADROOTS_CLI_PATHS_PROFILE"
- );
- assert_eq!(resolved.paths.root_source, "repo_local_root");
- assert_eq!(
- resolved.paths.repo_local_root,
- Some(PathBuf::from(
- "/workspaces/radroots-cli/.local/radroots/dev"
- ))
- );
- assert_eq!(
- resolved.paths.repo_local_root_source,
- Some("process_env:RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT".to_owned())
- );
- assert_eq!(
- resolved.paths.app_config_path,
- PathBuf::from(
- "/workspaces/radroots-cli/.local/radroots/dev/config/apps/cli/config.toml"
- )
- );
- assert_eq!(
- resolved.paths.workspace_config_path,
- Some(PathBuf::from(
- "/workspaces/radroots-cli/.local/radroots/dev/config.toml"
- ))
- );
- assert_eq!(
- resolved.paths.app_data_root,
- PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/data/apps/cli")
- );
- assert_eq!(
- resolved.paths.app_logs_root,
- PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/logs/apps/cli")
- );
- assert_eq!(
- resolved.paths.shared_accounts_data_root,
- PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/data/shared/accounts")
- );
- assert_eq!(
- resolved.paths.default_identity_path,
- PathBuf::from(
- "/workspaces/radroots-cli/.local/radroots/dev/secrets/shared/identities/default.json"
- )
- );
- }
-
- #[test]
- fn repo_local_profile_requires_explicit_root() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_PATHS_PROFILE".to_owned(),
- "repo_local".to_owned(),
- )]));
-
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("repo_local should require an explicit root");
- assert!(
- error
- .to_string()
- .contains("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT")
- );
- }
-
- #[test]
- fn env_file_can_select_repo_local_profile() {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::new());
- let env_file = parse_env_file_values(
- r#"
-RADROOTS_CLI_PATHS_PROFILE=repo_local
-RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT=.local/radroots/dev
-"#,
- Path::new(".env.test"),
- )
- .expect("parse env file");
-
- let resolved =
- RuntimeConfig::resolve_with_env_file(&args, &env, &env_file).expect("resolve config");
- assert_eq!(resolved.paths.profile, "repo_local");
- assert_eq!(
- resolved.paths.app_data_root,
- PathBuf::from("/workspaces/radroots-cli/.local/radroots/dev/data/apps/cli")
- );
- assert_eq!(
- resolved.paths.workspace_config_path,
- Some(PathBuf::from(
- "/workspaces/radroots-cli/.local/radroots/dev/config.toml"
- ))
- );
- }
-
- #[test]
- fn unknown_env_file_variable_fails() {
- let error = parse_env_file_values(
- "RADROOTS_CLI_LOGGING_FILTRE=debug\n",
- Path::new(".env.test"),
- )
- .expect_err("unknown env variable");
- assert!(
- error
- .to_string()
- .contains("unknown environment variable `RADROOTS_CLI_LOGGING_FILTRE`")
- );
- }
-
- #[test]
- fn old_env_file_variable_fails() {
- let error = parse_env_file_values("RADROOTS_OUTPUT=json\n", Path::new(".env.test"))
- .expect_err("old env variable should fail");
- assert!(
- error
- .to_string()
- .contains("unknown environment variable `RADROOTS_OUTPUT`")
- );
- }
-
- #[test]
- fn duplicate_env_file_variable_fails() {
- let error = parse_env_file_values(
- "RADROOTS_CLI_OUTPUT_FORMAT=json\nRADROOTS_CLI_OUTPUT_FORMAT=terminal\n",
- Path::new(".env.test"),
- )
- .expect_err("duplicate env variable should fail");
- assert!(
- error
- .to_string()
- .contains("duplicate environment variable `RADROOTS_CLI_OUTPUT_FORMAT`")
- );
- }
-
- #[test]
- fn old_toml_groups_and_fields_fail() {
- let temp = tempdir().expect("tempdir");
- let workspace_root = temp.path().join("workspace");
- let repo_local_root = workspace_root.join("infra/local/runtime/radroots");
- let user_home = temp.path().join("home");
- fs::create_dir_all(&repo_local_root).expect("workspace config dir");
-
- for (raw, expected) in [
- (
- "[relay]\nurls = [\"wss://relay.old\"]\n",
- "unknown field `relay`",
- ),
- ("[signer]\nmode = \"local\"\n", "unknown field `mode`"),
- (
- "[relays]\nurls = [\"wss://relay.example\"]\nextra = true\n",
- "unknown field `relays`",
- ),
- ] {
- fs::write(repo_local_root.join("config.toml"), raw).expect("write config");
- let env = repo_local_env(
- workspace_root.clone(),
- repo_local_root.clone(),
- user_home.clone(),
- BTreeMap::new(),
- );
- let error = RuntimeConfig::resolve_with_env_file(
- &runtime_args(),
- &env,
- &EnvFileValues::default(),
- )
- .expect_err("old toml shape should fail");
- assert!(
- error.to_string().contains(expected),
- "expected {expected}, got {error}"
- );
- }
- }
-
- #[test]
- fn runtime_config_output_format_accepts_current_contract_values_and_rejects_human() {
- for (value, expected) in [
- ("terminal", OutputFormat::Terminal),
- ("json", OutputFormat::Json),
- ("ndjson", OutputFormat::Ndjson),
- ] {
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
- value.to_owned(),
- )]));
-
- let resolved =
- RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect("resolve runtime config");
- assert_eq!(resolved.output.format, expected);
- }
-
- let args = runtime_args();
- let env = MapEnvironment::new(BTreeMap::from([(
- "RADROOTS_CLI_OUTPUT_FORMAT".to_owned(),
- "human".to_owned(),
- )]));
- let error = RuntimeConfig::resolve_with_env_file(&args, &env, &EnvFileValues::default())
- .expect_err("human output format must stay removed");
- let message = error.to_string();
- assert!(message.contains("RADROOTS_CLI_OUTPUT_FORMAT"));
- assert!(message.contains("terminal"));
- assert!(message.contains("json"));
- assert!(message.contains("ndjson"));
- assert!(message.contains("human"));
- }
-}
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -1,2051 +0,0 @@
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_event::contract::AuthorRole;
-use radroots_event::envelope::kind::{KIND_FARM, KIND_PROFILE};
-use radroots_event::farm::{Farm, FarmPublicLocation};
-use radroots_event::id::AddressableCoordinate;
-use radroots_event::listing::operational::OperationalListingPublicLocation;
-use radroots_event::profile::{AuthoredProfile, Nip05Identifier};
-use radroots_event_codec::d_tag::is_d_tag_base64url;
-use radroots_event_codec::profile::authored::authored_profile_to_wire_parts;
-use radroots_sdk::farm::{self as sdk_farm, Plan as FarmPlan};
-use radroots_signing::{Actor, actor::ActorSource};
-use serde_json::json;
-
-use crate::cli::global::{
- FarmCreateArgs, FarmFieldArg, FarmPrivateLocationKeyArgs, FarmPrivateLocationSetArgs,
- FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::account::{self, AccountRecordView};
-use crate::runtime::config::{RuntimeConfig, SignerBackend, TransportProfileKind};
-use crate::runtime::farm_config::{
- self, FarmConfigDocument, FarmConfigScope, FarmConfigSelection, FarmListingDefaults,
- FarmMissingField, FarmProfileDraft, FarmPublicationStatus, ResolvedFarmConfig,
- SUPPORTED_FARM_CONFIG_VERSION,
-};
-use crate::runtime::runtime_store::append_local_work;
-use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor};
-use crate::runtime::signer::ActorWriteBindingError;
-use crate::view::runtime::{
- FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
- FarmPrivateLocationView, FarmProfileDraftView, FarmPublicationView, FarmPublishComponentView,
- FarmPublishEventView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView,
- FarmSetupView, FarmStatusView,
-};
-
-const FARM_CONFIG_SOURCE: &str = "farm config · local first";
-const FARM_SELLER_ACTOR_SOURCE: &str = "farm_config";
-const SDK_FARM_WRITE_SOURCE: &str = "SDK farm publish · configured signer";
-const SDK_PROFILE_NOT_SUBMITTED_METHOD: &str = "sdk.farm.profile.not_submitted";
-const SDK_FARM_PUBLISH_METHOD: &str = "sdk.farm.publish.v1";
-const SDK_FARM_PRIVATE_LOCATION_SOURCE: &str = "SDK private farm location · local private store";
-const SDK_PROFILE_NOT_SUBMITTED_REASON: &str =
- "profile publish is not part of SDK farm.publish.v1; profile draft was not submitted";
-
-static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0);
-
-pub fn init(config: &RuntimeConfig, args: &FarmCreateArgs) -> Result<FarmSetupView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let Some(selected_account) = selected_account_for_draft(config)? else {
- return Ok(missing_selected_account_setup_view());
- };
- let existing = farm_config::load(config, Some(resolved_scope))?;
- let document = init_document(resolved_scope, &selected_account, existing.as_ref(), args)?;
- save_draft_view(
- "saved",
- resolved_scope,
- &selected_account,
- &document,
- Some("The farm draft is local until you publish it.".to_owned()),
- farm_setup_actions(config, &document, Some(&selected_account)),
- config,
- )
-}
-
-pub fn init_preflight(
- config: &RuntimeConfig,
- args: &FarmCreateArgs,
-) -> Result<FarmSetupView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let Some(selected_account) = selected_account_for_draft(config)? else {
- return Ok(missing_selected_account_setup_view());
- };
- let existing = farm_config::load(config, Some(resolved_scope))?;
- let document = init_document(resolved_scope, &selected_account, existing.as_ref(), args)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- Ok(FarmSetupView {
- state: "dry_run".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- config: Some(summary_view(
- resolved_scope,
- path.display().to_string(),
- &document,
- Some(
- selected_account
- .record
- .public_identity()
- .public_key()
- .to_hex()
- .as_str(),
- ),
- )),
- reason: Some("dry run requested; farm draft was not written".to_owned()),
- actions: farm_setup_actions(config, &document, Some(&selected_account)),
- })
-}
-
-pub fn rebind(
- config: &RuntimeConfig,
- args: &FarmRebindArgs,
-) -> Result<FarmRebindView, RuntimeError> {
- rebind_inner(config, args, false)
-}
-
-pub fn rebind_preflight(
- config: &RuntimeConfig,
- args: &FarmRebindArgs,
-) -> Result<FarmRebindView, RuntimeError> {
- rebind_inner(config, args, true)
-}
-
-fn rebind_inner(
- config: &RuntimeConfig,
- args: &FarmRebindArgs,
- dry_run: bool,
-) -> Result<FarmRebindView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(FarmRebindView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved_scope.as_str().to_owned(),
- path: path.display().to_string(),
- config_present: false,
- dry_run,
- seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
- from_seller_account_id: None,
- from_seller_pubkey: None,
- to_seller_account_id: None,
- to_seller_pubkey: None,
- seller_pubkey_changed: None,
- publication_state_action: None,
- config: None,
- reason: Some(format!("no farm config found at {}", path.display())),
- actions: vec!["radroots farm create".to_owned()],
- });
- };
-
- let from_account = configured_account(config, &resolved.document.selection.account)?;
- let from_seller_pubkey = from_account
- .as_ref()
- .map(|account| account.record.public_identity().public_key().to_hex());
- let target_account = account::resolve_account_selector(config, args.selector.as_str())
- .map_err(|error| farm_rebind_selector_error(args.selector.as_str(), error))?;
- let to_seller_pubkey = target_account
- .record
- .public_identity()
- .public_key()
- .to_hex();
- let seller_pubkey_changed = from_seller_pubkey
- .as_deref()
- .is_none_or(|pubkey| !pubkey.eq_ignore_ascii_case(to_seller_pubkey.as_str()));
- let publication_state_action = if seller_pubkey_changed {
- "cleared"
- } else {
- "preserved"
- };
- let mut document = resolved.document.clone();
- document.selection.account = target_account.record.id().to_string();
- if seller_pubkey_changed {
- document.publication = FarmPublicationStatus::default();
- }
- let written_path = if dry_run {
- resolved.path.clone()
- } else {
- let written_path = farm_config::write(&config.paths, resolved.scope, &document)?;
- append_farm_local_work(
- config,
- resolved.scope,
- written_path.display().to_string(),
- &document,
- Some(to_seller_pubkey.as_str()),
- )?;
- written_path
- };
- let state = if dry_run { "dry_run" } else { "rebound" };
-
- Ok(FarmRebindView {
- state: state.to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved.scope.as_str().to_owned(),
- path: written_path.display().to_string(),
- config_present: true,
- dry_run,
- seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
- from_seller_account_id: Some(resolved.document.selection.account.clone()),
- from_seller_pubkey,
- to_seller_account_id: Some(target_account.record.id().to_string()),
- to_seller_pubkey: Some(to_seller_pubkey.clone()),
- seller_pubkey_changed: Some(seller_pubkey_changed),
- publication_state_action: Some(publication_state_action.to_owned()),
- config: Some(summary_view(
- resolved.scope,
- written_path.display().to_string(),
- &document,
- Some(to_seller_pubkey.as_str()),
- )),
- reason: Some(if dry_run {
- "dry run requested; farm seller binding was not written".to_owned()
- } else {
- "farm seller binding updated".to_owned()
- }),
- actions: if dry_run {
- vec![farm_rebind_live_action(args.selector.as_str())]
- } else {
- vec!["radroots farm get".to_owned()]
- },
- })
-}
-
-fn farm_rebind_selector_error(selector: &str, error: RuntimeError) -> RuntimeError {
- match error {
- RuntimeError::Account(account::AccountRuntimeFailure::Unresolved(issue)) => {
- account::AccountRuntimeFailure::unresolved_with_detail(
- issue.message().to_owned(),
- json!({
- "seller_actor_source": FARM_SELLER_ACTOR_SOURCE,
- "selector": selector,
- "actions": account_recovery_actions(),
- }),
- )
- .into()
- }
- other => other,
- }
-}
-
-pub fn set(config: &RuntimeConfig, args: &FarmUpdateArgs) -> Result<FarmSetView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(mut resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(FarmSetView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- field: terminal_field_name(args.field).to_owned(),
- value: terminal_field_value(args.field, args.value.join(" ").trim()).to_owned(),
- config: None,
- reason: Some(format!("no farm draft found at {}", path.display())),
- actions: vec!["radroots farm create".to_owned()],
- });
- };
-
- let raw_value = args.value.join(" ");
- let field_value = required_text(raw_value.as_str(), "farm set value")?;
- apply_field_update(&mut resolved.document, args.field, field_value.as_str())?;
- let written_path = farm_config::write(&config.paths, resolved.scope, &resolved.document)?;
- let configured_account = configured_account(config, &resolved.document.selection.account)?;
- let account_public_key_hex = configured_account
- .as_ref()
- .map(|account| account.record.public_identity().public_key().to_hex());
- let account_pubkey = account_public_key_hex.as_deref();
- append_farm_local_work(
- config,
- resolved.scope,
- written_path.display().to_string(),
- &resolved.document,
- account_pubkey,
- )?;
- let reason = if configured_account.is_none() {
- Some(missing_farm_bound_seller_reason(
- resolved.document.selection.account.as_str(),
- ))
- } else {
- None
- };
-
- Ok(FarmSetView {
- state: "updated".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- field: terminal_field_name(args.field).to_owned(),
- value: terminal_field_value(args.field, field_value.as_str()).to_owned(),
- config: Some(summary_view(
- resolved.scope,
- written_path.display().to_string(),
- &resolved.document,
- account_pubkey,
- )),
- reason,
- actions: farm_update_actions(config, &resolved.document, configured_account.as_ref()),
- })
-}
-
-pub fn set_preflight(
- config: &RuntimeConfig,
- args: &FarmUpdateArgs,
-) -> Result<FarmSetView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(mut resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(FarmSetView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- field: terminal_field_name(args.field).to_owned(),
- value: terminal_field_value(args.field, args.value.join(" ").trim()).to_owned(),
- config: None,
- reason: Some(format!("no farm draft found at {}", path.display())),
- actions: vec!["radroots farm create".to_owned()],
- });
- };
-
- let raw_value = args.value.join(" ");
- let field_value = required_text(raw_value.as_str(), "farm set value")?;
- apply_field_update(&mut resolved.document, args.field, field_value.as_str())?;
- let configured_account = configured_account(config, &resolved.document.selection.account)?;
- let account_public_key_hex = configured_account
- .as_ref()
- .map(|account| account.record.public_identity().public_key().to_hex());
- let account_pubkey = account_public_key_hex.as_deref();
- let reason = if configured_account.is_none() {
- Some(format!(
- "dry run requested; farm draft was not written; {}",
- missing_farm_bound_seller_reason(resolved.document.selection.account.as_str())
- ))
- } else {
- Some("dry run requested; farm draft was not written".to_owned())
- };
-
- Ok(FarmSetView {
- state: "dry_run".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- field: terminal_field_name(args.field).to_owned(),
- value: terminal_field_value(args.field, field_value.as_str()).to_owned(),
- config: Some(summary_view(
- resolved.scope,
- path.display().to_string(),
- &resolved.document,
- account_pubkey,
- )),
- reason,
- actions: farm_update_actions(config, &resolved.document, configured_account.as_ref()),
- })
-}
-
-pub fn private_location_set(
- config: &RuntimeConfig,
- args: &FarmPrivateLocationSetArgs,
-) -> Result<FarmPrivateLocationView, CliSdkAdapterError> {
- match private_location_target(config, args.farm_d_tag.as_deref())? {
- Some(_) => Err(RuntimeError::Config(
- "private farm location writes require a host-owned private artifact adapter".to_owned(),
- )
- .into()),
- None => Ok(private_location_unconfigured_view()),
- }
-}
-
-pub fn private_location_get(
- config: &RuntimeConfig,
- args: &FarmPrivateLocationKeyArgs,
-) -> Result<FarmPrivateLocationView, CliSdkAdapterError> {
- match private_location_target(config, args.farm_d_tag.as_deref())? {
- Some(_) => Err(RuntimeError::Config(
- "private farm location reads require a host-owned private artifact adapter".to_owned(),
- )
- .into()),
- None => Ok(private_location_unconfigured_view()),
- }
-}
-
-pub fn private_location_clear(
- config: &RuntimeConfig,
- args: &FarmPrivateLocationKeyArgs,
-) -> Result<FarmPrivateLocationView, CliSdkAdapterError> {
- match private_location_target(config, args.farm_d_tag.as_deref())? {
- Some(_) => Err(RuntimeError::Config(
- "private farm location deletion requires a host-owned private artifact adapter"
- .to_owned(),
- )
- .into()),
- None => Ok(private_location_unconfigured_view()),
- }
-}
-
-pub fn status(
- config: &RuntimeConfig,
- args: &FarmScopedArgs,
-) -> Result<FarmStatusView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(FarmStatusView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved_scope.as_str().to_owned(),
- path: path.display().to_string(),
- config_present: false,
- config_valid: false,
- account_state: "not_checked".to_owned(),
- listing_defaults_state: "missing".to_owned(),
- transport_profile: config.transport.profile.as_str().to_owned(),
- publish_state: "not_checked".to_owned(),
- publish_executable: false,
- publish_reason: None,
- config: None,
- missing: vec!["Farm draft".to_owned()],
- reason: Some(format!("no farm config found at {}", path.display())),
- actions: vec!["radroots farm create".to_owned()],
- });
- };
-
- let account = configured_account(config, &resolved.document.selection.account)?;
- let draft_missing = farm_config::missing_fields(&resolved.document);
- let account_state = if account.is_some() {
- "ready"
- } else {
- "missing"
- };
- let listing_defaults_state = if missing_blocks_listing_defaults(draft_missing.as_slice()) {
- "missing"
- } else {
- "ready"
- };
- let publish = account
- .as_ref()
- .filter(|_| draft_missing.is_empty())
- .map(|account| farm_publish_readiness(config, account))
- .unwrap_or_else(FarmPublishReadiness::not_checked);
- let state = if account.is_some() && draft_missing.is_empty() && publish.executable {
- "ready"
- } else {
- "unconfigured"
- };
- let reason = if account.is_none() {
- Some(format!(
- "farm config account `{}` is not present in the local account store",
- resolved.document.selection.account
- ))
- } else if !draft_missing.is_empty() {
- Some("farm draft is missing required fields".to_owned())
- } else {
- publish.reason.clone()
- };
- let mut actions = Vec::new();
- if account.is_none() {
- actions.extend(farm_bound_seller_recovery_actions("<selector>"));
- } else if draft_missing.is_empty() {
- actions.extend(publish.actions.clone());
- } else {
- actions.extend(missing_field_actions(draft_missing.as_slice()));
- }
- let account_public_key_hex = account
- .as_ref()
- .map(|account| account.record.public_identity().public_key().to_hex());
- let account_pubkey = account_public_key_hex.as_deref();
-
- Ok(FarmStatusView {
- state: state.to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved.scope.as_str().to_owned(),
- path: resolved.path.display().to_string(),
- config_present: true,
- config_valid: true,
- account_state: account_state.to_owned(),
- listing_defaults_state: listing_defaults_state.to_owned(),
- transport_profile: config.transport.profile.as_str().to_owned(),
- publish_state: publish.state.to_owned(),
- publish_executable: publish.executable,
- publish_reason: publish.reason,
- config: Some(summary_view(
- resolved.scope,
- resolved.path.display().to_string(),
- &resolved.document,
- account_pubkey,
- )),
- missing: if account.is_none() {
- vec!["Farm-bound seller account".to_owned()]
- } else {
- let mut missing = missing_field_labels(draft_missing.as_slice());
- missing.extend(publish.missing);
- missing
- },
- reason,
- actions,
- })
-}
-
-pub fn get(config: &RuntimeConfig, args: &FarmScopedArgs) -> Result<FarmGetView, RuntimeError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(FarmGetView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved_scope.as_str().to_owned(),
- path: path.display().to_string(),
- config_present: false,
- document: None,
- reason: Some(format!("no farm config found at {}", path.display())),
- actions: vec!["radroots farm create".to_owned()],
- });
- };
-
- Ok(FarmGetView {
- state: "ready".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- scope: resolved.scope.as_str().to_owned(),
- path: resolved.path.display().to_string(),
- config_present: true,
- document: Some(document_view(&resolved.document)),
- reason: None,
- actions: Vec::new(),
- })
-}
-
-#[derive(Debug, Clone)]
-struct FarmPublishReadiness {
- state: &'static str,
- executable: bool,
- reason: Option<String>,
- missing: Vec<String>,
- actions: Vec<String>,
-}
-
-impl FarmPublishReadiness {
- fn not_checked() -> Self {
- Self {
- state: "not_checked",
- executable: false,
- reason: None,
- missing: Vec::new(),
- actions: Vec::new(),
- }
- }
-}
-
-fn farm_publish_readiness(
- config: &RuntimeConfig,
- account: &AccountRecordView,
-) -> FarmPublishReadiness {
- transport_farm_publish_readiness(config, account)
-}
-
-fn transport_farm_publish_readiness(
- config: &RuntimeConfig,
- account: &AccountRecordView,
-) -> FarmPublishReadiness {
- if matches!(
- config.transport.profile,
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
- ) && config.transport.nostr_relay_urls.is_empty()
- {
- return FarmPublishReadiness {
- state: "unconfigured",
- executable: false,
- reason: Some("farm publish requires a configured Nostr transport profile".to_owned()),
- missing: vec!["Configured Nostr transport profile".to_owned()],
- actions: vec![
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
- .to_owned(),
- ],
- };
- }
-
- if matches!(config.signer.backend, SignerBackend::Myc) {
- if let Err(error) = validate_configured_signer_for_actor(
- config,
- Some(account.record.id().to_hex().as_str()),
- account
- .record
- .public_identity()
- .public_key()
- .to_hex()
- .as_str(),
- "farm seller",
- ) {
- return FarmPublishReadiness {
- state: "unconfigured",
- executable: false,
- reason: Some(error.to_string()),
- missing: vec!["Remote signer binding".to_owned()],
- actions: vec!["radroots signer status".to_owned()],
- };
- }
- return FarmPublishReadiness {
- state: "ready",
- executable: true,
- reason: None,
- missing: Vec::new(),
- actions: Vec::new(),
- };
- }
-
- if !account.write_capable {
- return FarmPublishReadiness {
- state: "unconfigured",
- executable: false,
- reason: Some(
- account::AccountRuntimeFailure::watch_only(&account.record.id()).to_string(),
- ),
- missing: vec!["Write-capable farm-bound seller account".to_owned()],
- actions: vec!["radroots account create".to_owned()],
- };
- }
-
- FarmPublishReadiness {
- state: "ready",
- executable: true,
- reason: None,
- missing: Vec::new(),
- actions: vec!["radroots farm publish".to_owned()],
- }
-}
-
-pub fn publish(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
-) -> Result<FarmPublishView, CliSdkAdapterError> {
- let scope = scope_from_arg(args.scope);
- let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
- let path = farm_config::config_path(&config.paths, resolved_scope)?;
- let Some(resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(missing_publish_view(
- config,
- resolved_scope,
- path.display().to_string(),
- args,
- format!("no farm config found at {}", path.display()),
- vec!["Farm draft".to_owned()],
- vec!["radroots farm create".to_owned()],
- config.output.dry_run,
- false,
- String::new(),
- String::new(),
- String::new(),
- ));
- };
-
- let Some(account) = configured_account(config, &resolved.document.selection.account)? else {
- return Ok(missing_publish_view(
- config,
- resolved.scope,
- resolved.path.display().to_string(),
- args,
- format!(
- "farm config account `{}` is not present in the local account store",
- resolved.document.selection.account
- ),
- vec!["Farm-bound seller account".to_owned()],
- farm_bound_seller_recovery_actions("<selector>"),
- config.output.dry_run,
- true,
- resolved.document.selection.account.clone(),
- String::new(),
- resolved.document.selection.farm_d_tag.clone(),
- ));
- };
- let draft_missing = farm_config::missing_fields(&resolved.document);
- if !draft_missing.is_empty() {
- return Ok(missing_publish_view(
- config,
- resolved.scope,
- resolved.path.display().to_string(),
- args,
- "farm draft is missing required fields".to_owned(),
- missing_field_labels(draft_missing.as_slice()),
- missing_field_actions(draft_missing.as_slice()),
- config.output.dry_run,
- true,
- resolved.document.selection.account.clone(),
- account.record.public_identity().public_key().to_hex(),
- resolved.document.selection.farm_d_tag.clone(),
- ));
- }
- let account_pubkey = account.record.public_identity().public_key().to_hex();
- let previews = build_publish_previews(&resolved.document, account_pubkey.as_str())?;
- let profile_idempotency_key = component_idempotency_key(args, "profile")?;
- let farm_idempotency_key = component_idempotency_key(args, "farm")?;
-
- publish_via_sdk(
- config,
- args,
- resolved,
- account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- )
-}
-
-fn publish_via_sdk(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- mut resolved: ResolvedFarmConfig,
- account_pubkey: String,
- previews: FarmPublishPreviews,
- profile_idempotency_key: Option<String>,
- farm_idempotency_key: Option<String>,
-) -> Result<FarmPublishView, CliSdkAdapterError> {
- let input = sdk_farm_publish_input(&resolved, account_pubkey.as_str())?;
- if config.output.dry_run {
- if let Err(error) = validate_configured_signer_for_actor(
- config,
- Some(resolved.document.selection.account.as_str()),
- account_pubkey.as_str(),
- "farm seller",
- ) {
- let binding_error = ActorWriteBindingError::from_runtime(error);
- return match binding_error {
- ActorWriteBindingError::Account(failure) => Err(RuntimeError::from(failure).into()),
- error => Ok(binding_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- )),
- };
- }
-
- let plan = sdk_farm::prepare(sdk_farm::PrepareRequest::new(
- input.actor,
- input.farm,
- now_unix(),
- ))
- .map_err(|error| RuntimeError::Config(format!("invalid SDK farm plan: {error}")))?;
- return Ok(sdk_prepared_publish_view(
- config,
- args,
- &resolved,
- account_pubkey.as_str(),
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- plan,
- ));
- }
-
- Err(RuntimeError::Config(
- "farm commit is unavailable until the shared sync engine is configured".to_owned(),
- )
- .into())
-}
-
-#[derive(Debug, Clone)]
-struct SdkFarmPublishInput {
- actor: Actor,
- farm: Farm,
-}
-
-#[derive(Debug, Clone)]
-struct PrivateFarmLocationTarget {
- actor: Actor,
- farm_addr: AddressableCoordinate,
- farm_d_tag: String,
- seller_account_id: String,
- seller_pubkey: String,
-}
-
-#[derive(Debug, Clone)]
-struct FarmPublishPreviews {
- profile: FarmPublishEventDraft,
-}
-
-#[derive(Debug, Clone)]
-struct FarmPublishEventDraft {
- event: FarmPublishEventView,
-}
-
-#[expect(
- clippy::too_many_arguments,
- reason = "publish view construction mirrors the V1 output contract fields"
-)]
-fn missing_publish_view(
- config: &RuntimeConfig,
- scope: FarmConfigScope,
- path: String,
- args: &FarmPublishArgs,
- reason: String,
- missing: Vec<String>,
- actions: Vec<String>,
- dry_run: bool,
- config_present: bool,
- seller_account_id: String,
- seller_pubkey: String,
- farm_d_tag: String,
-) -> FarmPublishView {
- FarmPublishView {
- state: "unconfigured".to_owned(),
- source: farm_write_source(config).to_owned(),
- scope: scope.as_str().to_owned(),
- path,
- config_present,
- dry_run,
- seller_account_id,
- seller_pubkey,
- seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
- farm_d_tag,
- profile: not_submitted_component(
- profile_publish_rpc_method(config),
- KIND_PROFILE,
- args,
- None,
- None,
- ),
- farm: not_submitted_component(farm_publish_rpc_method(config), KIND_FARM, args, None, None),
- local_replica: Vec::new(),
- missing,
- reason: Some(reason),
- actions,
- }
-}
-
-#[expect(
- clippy::too_many_arguments,
- reason = "publish view construction mirrors the V1 output contract fields"
-)]
-fn base_publish_view(
- state: &str,
- config: &RuntimeConfig,
- _args: &FarmPublishArgs,
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
- profile: FarmPublishComponentView,
- farm: FarmPublishComponentView,
- reason: Option<String>,
- actions: Vec<String>,
-) -> FarmPublishView {
- FarmPublishView {
- state: state.to_owned(),
- source: farm_write_source(config).to_owned(),
- scope: resolved.scope.as_str().to_owned(),
- path: resolved.path.display().to_string(),
- config_present: true,
- dry_run: config.output.dry_run,
- seller_account_id: resolved.document.selection.account.clone(),
- seller_pubkey: account_pubkey.to_owned(),
- seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
- farm_d_tag: resolved.document.selection.farm_d_tag.clone(),
- profile,
- farm,
- local_replica: Vec::new(),
- missing: Vec::new(),
- reason,
- actions,
- }
-}
-
-fn build_publish_previews(
- document: &FarmConfigDocument,
- account_pubkey: &str,
-) -> Result<FarmPublishPreviews, RuntimeError> {
- require_verified_publish_media(&document.profile, &document.farm)?;
- let profile = authored_profile_from_draft(&document.profile)?;
- let profile_parts = authored_profile_to_wire_parts(&profile)
- .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?;
-
- Ok(FarmPublishPreviews {
- profile: FarmPublishEventDraft {
- event: FarmPublishEventView {
- kind: profile_parts.kind,
- author: account_pubkey.to_owned(),
- content: profile_parts.content.clone(),
- tags: profile_parts.tags.clone(),
- event_id: None,
- event_addr: None,
- },
- },
- })
-}
-
-fn authored_profile_from_draft(draft: &FarmProfileDraft) -> Result<AuthoredProfile, RuntimeError> {
- let mut profile = AuthoredProfile::new(draft.name.clone())
- .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?;
- if let Some(display_name) = draft.display_name.as_deref().and_then(non_empty) {
- profile = profile.with_display_name(display_name);
- }
- if let Some(about) = draft.about.as_deref().and_then(non_empty) {
- profile = profile.with_about(about);
- }
- if let Some(nip05) = draft.nip05.as_deref().and_then(non_empty) {
- let identifier = Nip05Identifier::parse(nip05.as_str()).map_err(|error| {
- RuntimeError::Config(format!("invalid farm profile NIP-05 identifier: {error}"))
- })?;
- profile = profile.with_nip05(identifier);
- }
- if let Some(bot) = draft.bot.as_deref().and_then(non_empty) {
- let bot = match bot.as_str() {
- "true" => true,
- "false" => false,
- _ => {
- return Err(RuntimeError::Config(
- "farm profile bot must be `true` or `false`".to_owned(),
- ));
- }
- };
- profile = profile.with_bot(bot);
- }
- Ok(profile)
-}
-
-fn require_verified_publish_media(
- profile: &FarmProfileDraft,
- farm: &Farm,
-) -> Result<(), RuntimeError> {
- let contains_unverified_media = [
- profile.picture.as_deref(),
- profile.banner.as_deref(),
- farm.picture.as_deref(),
- farm.banner.as_deref(),
- ]
- .into_iter()
- .flatten()
- .any(|value| !value.trim().is_empty());
- if contains_unverified_media {
- return Err(RuntimeError::Config(
- "farm publish cannot use raw picture or banner URLs; publish media only after byte-verifying the Blossom descriptor and retaining proof of successful BUD-02 upload completion"
- .to_owned(),
- ));
- }
- Ok(())
-}
-
-fn component_idempotency_key(
- args: &FarmPublishArgs,
- component: &str,
-) -> Result<Option<String>, RuntimeError> {
- args.idempotency_key
- .as_deref()
- .map(|value| {
- required_text(value, "idempotency_key")
- .and_then(|key| derive_component_idempotency_key(key.as_str(), component))
- })
- .transpose()
-}
-
-fn derive_component_idempotency_key(key: &str, component: &str) -> Result<String, RuntimeError> {
- if !is_uuid_v7_idempotency_key(key) {
- return Err(RuntimeError::Config(
- "idempotency_key must be a UUIDv7".to_owned(),
- ));
- }
- let mut hash = 0xcbf2_9ce4_8422_2325_u64;
- for byte in key
- .bytes()
- .chain(std::iter::once(b':'))
- .chain(component.bytes())
- {
- hash ^= u64::from(byte);
- hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
- }
- Ok(format!("{}{:012x}", &key[..24], hash & 0xffff_ffff_ffff))
-}
-
-fn is_uuid_v7_idempotency_key(value: &str) -> bool {
- let bytes = value.as_bytes();
- bytes.len() == 36
- && bytes[8] == b'-'
- && bytes[13] == b'-'
- && bytes[18] == b'-'
- && bytes[23] == b'-'
- && bytes[14] == b'7'
- && matches!(bytes[19], b'8' | b'9' | b'a' | b'b')
- && bytes
- .iter()
- .enumerate()
- .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
-}
-
-fn preview_component(
- rpc_method: &str,
- event_kind: u32,
- idempotency_key: Option<String>,
- args: &FarmPublishArgs,
- event: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- FarmPublishComponentView {
- state: if event.is_some() {
- "not_submitted".to_owned()
- } else {
- "unconfigured".to_owned()
- },
- rpc_method: rpc_method.to_owned(),
- event_kind,
- deduplicated: false,
- target_transport_endpoints: Vec::new(),
- attempted_transport_endpoints: Vec::new(),
- accepted_transport_endpoints: Vec::new(),
- failed_transport_targets: Vec::new(),
- job_id: None,
- job_status: None,
- signer_mode: None,
- event_id: None,
- event_addr: event.as_ref().and_then(|event| event.event_addr.clone()),
- idempotency_key: idempotency_key.clone(),
- reason: Some("not submitted".to_owned()),
- job: None,
- event: args.print_event.then_some(event).flatten(),
- }
-}
-
-fn not_submitted_component(
- rpc_method: &str,
- event_kind: u32,
- args: &FarmPublishArgs,
- idempotency_key: Option<String>,
- event: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- preview_component(rpc_method, event_kind, idempotency_key, args, event)
-}
-
-#[expect(
- clippy::too_many_arguments,
- reason = "publish view construction mirrors the V1 output contract fields"
-)]
-fn binding_error_publish_view(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
- previews: FarmPublishPreviews,
- profile_idempotency_key: Option<String>,
- farm_idempotency_key: Option<String>,
- error: ActorWriteBindingError,
-) -> FarmPublishView {
- let reason = error.reason();
- let state = "unconfigured".to_owned();
- let actions = vec!["run radroots signer status".to_owned()];
- base_publish_view(
- state.as_str(),
- config,
- args,
- resolved,
- account_pubkey,
- FarmPublishComponentView {
- state: state.clone(),
- reason: Some(reason.clone()),
- ..profile_not_submitted_component(
- profile_idempotency_key,
- args,
- Some(previews.profile.event),
- )
- },
- FarmPublishComponentView {
- state: state.clone(),
- reason: Some(reason.clone()),
- ..preview_component(
- farm_publish_rpc_method(config),
- KIND_FARM,
- farm_idempotency_key,
- args,
- None,
- )
- },
- Some(reason),
- actions,
- )
-}
-
-fn sdk_farm_publish_input(
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
-) -> Result<SdkFarmPublishInput, RuntimeError> {
- let actor = Actor::from_public_key_hex(
- account_pubkey,
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .map_err(|error| RuntimeError::Config(format!("invalid farm SDK actor: {error}")))?;
- Ok(SdkFarmPublishInput {
- actor,
- farm: resolved.document.farm.clone(),
- })
-}
-
-#[expect(
- clippy::too_many_arguments,
- reason = "publish view construction mirrors the V1 output contract fields"
-)]
-fn sdk_prepared_publish_view(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
- previews: FarmPublishPreviews,
- profile_idempotency_key: Option<String>,
- farm_idempotency_key: Option<String>,
- plan: FarmPlan,
-) -> FarmPublishView {
- base_publish_view(
- "dry_run",
- config,
- args,
- resolved,
- account_pubkey,
- profile_not_submitted_component(
- profile_idempotency_key,
- args,
- Some(previews.profile.event),
- ),
- FarmPublishComponentView {
- state: "not_submitted".to_owned(),
- reason: Some("dry run requested; SDK enqueue and transport push skipped".to_owned()),
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id: Some(plan.draft().expected_event_id().to_string()),
- event_addr: Some(plan.coordinate().as_str().to_owned()),
- event: args.print_event.then_some(sdk_plan_event_view(&plan)),
- ..preview_component(
- farm_publish_rpc_method(config),
- KIND_FARM,
- farm_idempotency_key,
- args,
- None,
- )
- },
- Some("dry run requested; SDK enqueue and transport push skipped".to_owned()),
- vec!["radroots farm publish".to_owned()],
- )
-}
-
-fn sdk_plan_event_view(plan: &FarmPlan) -> FarmPublishEventView {
- FarmPublishEventView {
- kind: plan.draft().kind_u32(),
- author: plan.draft().expected_pubkey().to_hex(),
- content: plan.draft().content().to_owned(),
- tags: plan.draft().tags_as_vec(),
- event_id: Some(plan.draft().expected_event_id().to_string()),
- event_addr: Some(plan.coordinate().as_str().to_owned()),
- }
-}
-
-fn profile_not_submitted_component(
- idempotency_key: Option<String>,
- args: &FarmPublishArgs,
- event: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- FarmPublishComponentView {
- reason: Some(SDK_PROFILE_NOT_SUBMITTED_REASON.to_owned()),
- ..preview_component(
- SDK_PROFILE_NOT_SUBMITTED_METHOD,
- KIND_PROFILE,
- idempotency_key,
- args,
- event,
- )
- }
-}
-
-fn farm_write_source(config: &RuntimeConfig) -> &'static str {
- let _ = config;
- SDK_FARM_WRITE_SOURCE
-}
-
-fn profile_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
- let _ = config;
- SDK_PROFILE_NOT_SUBMITTED_METHOD
-}
-
-fn farm_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
- let _ = config;
- SDK_FARM_PUBLISH_METHOD
-}
-
-fn selected_account_for_draft(
- config: &RuntimeConfig,
-) -> Result<Option<AccountRecordView>, RuntimeError> {
- account::resolve_account(config)
-}
-
-fn private_location_target(
- config: &RuntimeConfig,
- farm_d_tag: Option<&str>,
-) -> Result<Option<PrivateFarmLocationTarget>, RuntimeError> {
- let resolved_scope = farm_config::resolve_scope(&config.paths, None)?;
- let Some(resolved) = farm_config::load(config, Some(resolved_scope))? else {
- return Ok(None);
- };
- let Some(account) = configured_account(config, &resolved.document.selection.account)? else {
- return Ok(None);
- };
- let seller_pubkey = account.record.public_identity().public_key().to_hex();
- let seller_account_id = account.record.id().to_string();
- let farm_d_tag = farm_d_tag
- .map(str::to_owned)
- .unwrap_or_else(|| resolved.document.selection.farm_d_tag.clone());
- let actor = Actor::from_public_key_hex(
- seller_pubkey.as_str(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .map_err(|error| {
- RuntimeError::Config(format!("invalid farm private location actor: {error}"))
- })?;
- let farm_addr =
- AddressableCoordinate::parse(format!("{KIND_FARM}:{seller_pubkey}:{farm_d_tag}"))
- .map_err(|error| RuntimeError::Config(format!("invalid farm address: {error}")))?;
- Ok(Some(PrivateFarmLocationTarget {
- actor,
- farm_addr,
- farm_d_tag,
- seller_account_id,
- seller_pubkey,
- }))
-}
-
-fn missing_selected_account_setup_view() -> FarmSetupView {
- FarmSetupView {
- state: "unconfigured".to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- config: None,
- reason: Some("choose or create an account before setting up your farm".to_owned()),
- actions: vec!["radroots account create".to_owned()],
- }
-}
-
-fn private_location_unconfigured_view() -> FarmPrivateLocationView {
- FarmPrivateLocationView {
- state: "unconfigured".to_owned(),
- source: SDK_FARM_PRIVATE_LOCATION_SOURCE.to_owned(),
- farm_addr: None,
- farm_d_tag: None,
- seller_account_id: None,
- seller_pubkey: None,
- label: None,
- exact_location: None,
- public_locality: None,
- geonames_feature_id: None,
- geonames_country_id: None,
- geonames_database_path: None,
- cleared: None,
- candidates: Vec::new(),
- reason: Some("create and bind a farm before storing private location".to_owned()),
- actions: vec![
- "radroots account create".to_owned(),
- "radroots farm create".to_owned(),
- ],
- }
-}
-
-fn init_document(
- scope: FarmConfigScope,
- account: &AccountRecordView,
- existing: Option<&ResolvedFarmConfig>,
- args: &FarmCreateArgs,
-) -> Result<FarmConfigDocument, RuntimeError> {
- let existing_document = existing.map(|resolved| &resolved.document);
- if let Some(document) = existing_document
- && document.selection.account != account.record.id().to_string()
- {
- let message = format!(
- "account mismatch: farm config is bound to seller account `{}`; select account `{}` before updating this farm config",
- document.selection.account,
- account.record.id()
- );
- return Err(account::AccountRuntimeFailure::mismatch_with_detail(
- message,
- json!({
- "seller_actor_source": FARM_SELLER_ACTOR_SOURCE,
- "farm_bound_seller_account_id": document.selection.account,
- "attempted_seller_account_id": account.record.id().to_string(),
- "actions": farm_rebind_recovery_actions(account.record.id().to_hex().as_str()),
- }),
- )
- .into());
- }
- let farm_d_tag = match args.farm_d_tag.as_deref() {
- Some(value) => required_d_tag(value, "farm_d_tag")?,
- None => existing_document
- .map(|document| document.farm.d_tag.clone())
- .unwrap_or_else(generate_d_tag),
- };
- let existing_name = existing_name(existing_document);
- let existing_location = existing_location_primary(existing_document);
- let existing_city = existing_city(existing_document);
- let existing_region = existing_region(existing_document);
- let existing_country = existing_country(existing_document);
- let existing_geohash = existing_geohash(existing_document);
- let existing_delivery = existing_delivery_method(existing_document);
- let name = optional_arg_or_existing(args.name.as_ref(), existing_name.as_ref())
- .or_else(|| draft_name_from_account(account))
- .unwrap_or_default();
- let display_name = optional_arg_or_existing(
- args.display_name.as_ref(),
- existing_document.and_then(|document| document.profile.display_name.as_ref()),
- )
- .or_else(|| non_empty(name.as_str()));
- let about = optional_arg_or_existing(
- args.about.as_ref(),
- existing_document.and_then(|document| document.profile.about.as_ref()),
- );
- let website = optional_arg_or_existing(
- args.website.as_ref(),
- existing_document.and_then(|document| document.profile.website.as_ref()),
- );
- let picture = optional_arg_or_existing(
- args.picture.as_ref(),
- existing_document.and_then(|document| document.profile.picture.as_ref()),
- );
- let banner = optional_arg_or_existing(
- args.banner.as_ref(),
- existing_document.and_then(|document| document.profile.banner.as_ref()),
- );
- let location_primary =
- optional_arg_or_existing(args.location.as_ref(), existing_location.as_ref())
- .unwrap_or_default();
- let city = optional_arg_or_existing(args.city.as_ref(), existing_city.as_ref());
- let region = optional_arg_or_existing(args.region.as_ref(), existing_region.as_ref());
- let country = optional_arg_or_existing(args.country.as_ref(), existing_country.as_ref());
- let geohash = optional_arg_or_existing(args.geohash.as_ref(), existing_geohash.as_ref())
- .unwrap_or_default();
- let delivery_method =
- optional_arg_or_existing(args.delivery_method.as_ref(), existing_delivery.as_ref())
- .unwrap_or_default();
- let publication = publication_for_document(existing_document, account, farm_d_tag.as_str());
-
- Ok(FarmConfigDocument {
- version: SUPPORTED_FARM_CONFIG_VERSION,
- selection: FarmConfigSelection {
- scope,
- account: account.record.id().to_string(),
- farm_d_tag: farm_d_tag.clone(),
- },
- profile: FarmProfileDraft {
- name: name.clone(),
- display_name,
- nip05: None,
- about: about.clone(),
- website: website.clone(),
- picture: picture.clone(),
- banner: banner.clone(),
- lud06: None,
- lud16: None,
- bot: None,
- },
- farm: Farm {
- d_tag: farm_d_tag,
- name,
- about,
- website,
- picture,
- banner,
- location: Some(FarmPublicLocation {
- primary: location_primary.clone(),
- city: city.clone(),
- region: region.clone(),
- country: country.clone(),
- geohash: geohash.clone(),
- }),
- tags: None,
- },
- listing_defaults: FarmListingDefaults {
- delivery_method,
- location: OperationalListingPublicLocation {
- primary: location_primary,
- city,
- region,
- country,
- geohash,
- },
- },
- publication,
- })
-}
-
-fn save_draft_view(
- state: &str,
- scope: FarmConfigScope,
- account: &AccountRecordView,
- document: &FarmConfigDocument,
- reason: Option<String>,
- actions: Vec<String>,
- config: &RuntimeConfig,
-) -> Result<FarmSetupView, RuntimeError> {
- let written_path = farm_config::write(&config.paths, scope, document)?;
- append_farm_local_work(
- config,
- scope,
- written_path.display().to_string(),
- document,
- Some(
- account
- .record
- .public_identity()
- .public_key()
- .to_hex()
- .as_str(),
- ),
- )?;
- Ok(FarmSetupView {
- state: state.to_owned(),
- source: FARM_CONFIG_SOURCE.to_owned(),
- config: Some(summary_view(
- scope,
- written_path.display().to_string(),
- document,
- Some(
- account
- .record
- .public_identity()
- .public_key()
- .to_hex()
- .as_str(),
- ),
- )),
- reason,
- actions,
- })
-}
-
-fn append_farm_local_work(
- config: &RuntimeConfig,
- scope: FarmConfigScope,
- path: String,
- document: &FarmConfigDocument,
- owner_pubkey: Option<&str>,
-) -> Result<(), RuntimeError> {
- let payload = json!({
- "record_kind": "farm_config_v1",
- "scope": scope.as_str(),
- "path": path,
- "document": document,
- });
- let subject = format!("farm:{}", document.selection.farm_d_tag);
- append_local_work(
- config,
- subject.as_str(),
- Some(document.selection.account.clone()),
- owner_pubkey.map(str::to_owned),
- Some(document.selection.farm_d_tag.clone()),
- None,
- payload,
- )?;
- Ok(())
-}
-
-fn farm_update_actions(
- config: &RuntimeConfig,
- document: &FarmConfigDocument,
- account: Option<&AccountRecordView>,
-) -> Vec<String> {
- farm_setup_actions(config, document, account)
-}
-
-fn farm_setup_actions(
- config: &RuntimeConfig,
- document: &FarmConfigDocument,
- account: Option<&AccountRecordView>,
-) -> Vec<String> {
- let mut actions = vec!["radroots farm get".to_owned()];
- if account.is_none() {
- actions.extend(farm_bound_seller_recovery_actions("<selector>"));
- return actions;
- }
- if farm_config::missing_fields(document).is_empty()
- && account
- .map(|account| farm_publish_readiness(config, account).executable)
- .unwrap_or(false)
- {
- actions.push("radroots farm publish".to_owned());
- }
- actions
-}
-
-fn missing_farm_bound_seller_reason(account_id: &str) -> String {
- format!("farm-bound seller account `{account_id}` is not present in the local account store")
-}
-
-pub(crate) fn farm_bound_seller_recovery_actions(selector: &str) -> Vec<String> {
- let mut actions = vec!["radroots account import <path>".to_owned()];
- actions.extend(farm_rebind_recovery_actions(selector));
- actions
-}
-
-pub(crate) fn farm_rebind_recovery_actions(selector: &str) -> Vec<String> {
- vec![
- farm_rebind_dry_run_action(selector),
- farm_rebind_live_action(selector),
- ]
-}
-
-pub(crate) fn farm_rebind_dry_run_action(selector: &str) -> String {
- format!("radroots --dry-run account select {selector}")
-}
-
-pub(crate) fn farm_rebind_live_action(selector: &str) -> String {
- format!("radroots account select {selector}")
-}
-
-fn account_recovery_actions() -> Vec<String> {
- vec![
- "radroots account import <path>".to_owned(),
- "radroots account create".to_owned(),
- ]
-}
-
-fn missing_blocks_listing_defaults(missing: &[FarmMissingField]) -> bool {
- missing.iter().any(|field| {
- matches!(
- field,
- FarmMissingField::Location
- | FarmMissingField::City
- | FarmMissingField::Delivery
- | FarmMissingField::Geohash
- )
- })
-}
-
-fn missing_field_labels(missing: &[FarmMissingField]) -> Vec<String> {
- missing
- .iter()
- .map(|field| field.label().to_owned())
- .collect()
-}
-
-fn missing_field_actions(missing: &[FarmMissingField]) -> Vec<String> {
- let mut actions = Vec::new();
- for field in missing {
- match field {
- FarmMissingField::Name => {
- push_action(&mut actions, "radroots farm set name \"La Huerta Farm\"");
- }
- FarmMissingField::Location => {
- push_action(
- &mut actions,
- "radroots farm set location \"San Francisco, CA\"",
- );
- }
- FarmMissingField::City => {
- push_action(&mut actions, "radroots farm set city \"San Francisco\"");
- }
- FarmMissingField::Delivery => {
- push_action(&mut actions, "radroots farm set delivery pickup");
- }
- FarmMissingField::Country => {
- push_action(&mut actions, "radroots farm set country US");
- }
- FarmMissingField::Geohash => {
- push_action(&mut actions, "radroots farm set geohash 9q8yy");
- }
- }
- }
- actions
-}
-
-fn push_action(actions: &mut Vec<String>, action: &str) {
- if !actions.iter().any(|existing| existing == action) {
- actions.push(action.to_owned());
- }
-}
-
-fn terminal_field_name(field: FarmFieldArg) -> &'static str {
- match field {
- FarmFieldArg::Name => "Name",
- FarmFieldArg::DisplayName => "Display name",
- FarmFieldArg::About => "About",
- FarmFieldArg::Website => "Website",
- FarmFieldArg::Picture => "Picture",
- FarmFieldArg::Banner => "Banner",
- FarmFieldArg::Location => "Location",
- FarmFieldArg::City => "City",
- FarmFieldArg::Region => "Region",
- FarmFieldArg::Country => "Country",
- FarmFieldArg::Geohash => "Geohash",
- FarmFieldArg::Delivery => "Delivery",
- }
-}
-
-fn terminal_field_value(field: FarmFieldArg, value: &str) -> String {
- match field {
- FarmFieldArg::Delivery => display_delivery_method(value),
- _ => value.to_owned(),
- }
-}
-
-fn apply_field_update(
- document: &mut FarmConfigDocument,
- field: FarmFieldArg,
- value: &str,
-) -> Result<(), RuntimeError> {
- let value = required_text(value, "farm set value")?;
- match field {
- FarmFieldArg::Name => {
- document.profile.name = value.clone();
- document.farm.name = value;
- }
- FarmFieldArg::DisplayName => {
- document.profile.display_name = Some(value);
- }
- FarmFieldArg::About => {
- document.profile.about = Some(value.clone());
- document.farm.about = Some(value);
- }
- FarmFieldArg::Website => {
- document.profile.website = Some(value.clone());
- document.farm.website = Some(value);
- }
- FarmFieldArg::Picture => {
- document.profile.picture = Some(value.clone());
- document.farm.picture = Some(value);
- }
- FarmFieldArg::Banner => {
- document.profile.banner = Some(value.clone());
- document.farm.banner = Some(value);
- }
- FarmFieldArg::Location => {
- document.listing_defaults.location.primary = value.clone();
- ensure_farm_location(document).primary = value;
- }
- FarmFieldArg::City => {
- document.listing_defaults.location.city = Some(value.clone());
- ensure_farm_location(document).city = Some(value);
- }
- FarmFieldArg::Region => {
- document.listing_defaults.location.region = Some(value.clone());
- ensure_farm_location(document).region = Some(value);
- }
- FarmFieldArg::Country => {
- document.listing_defaults.location.country = Some(value.clone());
- ensure_farm_location(document).country = Some(value);
- }
- FarmFieldArg::Geohash => {
- document.listing_defaults.location.geohash = value.clone();
- ensure_farm_location(document).geohash = value;
- }
- FarmFieldArg::Delivery => {
- document.listing_defaults.delivery_method = value;
- }
- }
- Ok(())
-}
-
-fn ensure_farm_location(document: &mut FarmConfigDocument) -> &mut FarmPublicLocation {
- let primary = document.listing_defaults.location.primary.clone();
- let city = document.listing_defaults.location.city.clone();
- let region = document.listing_defaults.location.region.clone();
- let country = document.listing_defaults.location.country.clone();
- let geohash = document.listing_defaults.location.geohash.clone();
- document.farm.location.get_or_insert(FarmPublicLocation {
- primary,
- city,
- region,
- country,
- geohash,
- })
-}
-
-fn publication_for_document(
- existing_document: Option<&FarmConfigDocument>,
- account: &AccountRecordView,
- farm_d_tag: &str,
-) -> FarmPublicationStatus {
- existing_document
- .filter(|document| {
- document.farm.d_tag == farm_d_tag
- && document.selection.account == account.record.id().to_hex()
- })
- .map(|document| document.publication.clone())
- .unwrap_or_default()
-}
-
-fn configured_account(
- config: &RuntimeConfig,
- account_id: &str,
-) -> Result<Option<AccountRecordView>, RuntimeError> {
- let snapshot = account::snapshot(config)?;
- Ok(snapshot
- .accounts
- .into_iter()
- .find(|account| account.record.id().to_hex() == account_id))
-}
-
-fn summary_view(
- scope: FarmConfigScope,
- path: String,
- document: &FarmConfigDocument,
- account_pubkey: Option<&str>,
-) -> FarmConfigSummaryView {
- FarmConfigSummaryView {
- scope: scope.as_str().to_owned(),
- path,
- seller_account_id: document.selection.account.clone(),
- seller_pubkey: account_pubkey.map(str::to_owned),
- seller_actor_source: FARM_SELLER_ACTOR_SOURCE.to_owned(),
- farm_d_tag: document.selection.farm_d_tag.clone(),
- name: resolved_name(document).unwrap_or_default(),
- location_primary: resolved_location_primary(document),
- delivery_method: resolved_delivery_method(document).unwrap_or_default(),
- publication: publication_view(&document.publication),
- }
-}
-
-fn document_view(document: &FarmConfigDocument) -> FarmConfigDocumentView {
- FarmConfigDocumentView {
- selection: FarmSelectionView {
- scope: document.selection.scope.as_str().to_owned(),
- seller_account_id: document.selection.account.clone(),
- farm_d_tag: document.selection.farm_d_tag.clone(),
- },
- profile: FarmProfileDraftView {
- name: document.profile.name.clone(),
- display_name: document.profile.display_name.clone(),
- nip05: document.profile.nip05.clone(),
- about: document.profile.about.clone(),
- website: document.profile.website.clone(),
- picture: document.profile.picture.clone(),
- banner: document.profile.banner.clone(),
- lud06: document.profile.lud06.clone(),
- lud16: document.profile.lud16.clone(),
- bot: document.profile.bot.clone(),
- },
- farm: document.farm.clone(),
- listing_defaults: FarmListingDefaultsView {
- delivery_method: document.listing_defaults.delivery_method.clone(),
- location: document.listing_defaults.location.clone(),
- },
- publication: publication_view(&document.publication),
- }
-}
-
-fn publication_view(publication: &FarmPublicationStatus) -> FarmPublicationView {
- FarmPublicationView {
- profile_state: publish_state(
- publication.profile_event_id.as_deref(),
- publication.profile_published_at,
- )
- .to_owned(),
- farm_state: publish_state(
- publication.farm_event_id.as_deref(),
- publication.farm_published_at,
- )
- .to_owned(),
- profile_event_id: publication.profile_event_id.clone(),
- farm_event_id: publication.farm_event_id.clone(),
- profile_published_at: publication.profile_published_at,
- farm_published_at: publication.farm_published_at,
- }
-}
-
-fn publish_state(event_id: Option<&str>, published_at: Option<u64>) -> &'static str {
- if event_id.is_some_and(|value| !value.trim().is_empty()) || published_at.is_some() {
- "published"
- } else {
- "not_published"
- }
-}
-
-fn scope_from_arg(scope: Option<FarmScopeArg>) -> Option<FarmConfigScope> {
- scope.map(|scope| match scope {
- FarmScopeArg::User => FarmConfigScope::User,
- FarmScopeArg::Workspace => FarmConfigScope::Workspace,
- })
-}
-
-fn required_d_tag(value: &str, field: &str) -> Result<String, RuntimeError> {
- let value = required_text(value, field)?;
- if !is_d_tag_base64url(value.as_str()) {
- return Err(RuntimeError::Config(format!(
- "{field} must be a 22-character base64url identifier"
- )));
- }
- Ok(value)
-}
-
-fn required_text(value: &str, field: &str) -> Result<String, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return Err(RuntimeError::Config(format!("{field} must not be empty")));
- }
- Ok(trimmed.to_owned())
-}
-
-fn optional_arg_or_existing(arg: Option<&String>, existing: Option<&String>) -> Option<String> {
- arg.and_then(|value| non_empty(value.as_str()))
- .or_else(|| existing.and_then(|value| non_empty(value.as_str())))
-}
-
-fn draft_name_from_account(account: &AccountRecordView) -> Option<String> {
- account
- .record
- .label()
- .and_then(non_empty)
- .or_else(|| non_empty(account.record.id().to_hex().as_str()))
-}
-
-fn existing_name(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document.and_then(resolved_name)
-}
-
-fn existing_location_primary(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document.and_then(resolved_location_primary)
-}
-
-fn existing_city(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document
- .and_then(|document| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| location.city.as_ref())
- })
- .and_then(|value| non_empty(value.as_str()))
- .or_else(|| {
- existing_document
- .and_then(|document| document.listing_defaults.location.city.as_ref())
- .and_then(|value| non_empty(value.as_str()))
- })
-}
-
-fn existing_region(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document
- .and_then(|document| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| location.region.as_ref())
- })
- .and_then(|value| non_empty(value.as_str()))
- .or_else(|| {
- existing_document
- .and_then(|document| document.listing_defaults.location.region.as_ref())
- .and_then(|value| non_empty(value.as_str()))
- })
-}
-
-fn existing_country(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document
- .and_then(|document| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| location.country.as_ref())
- })
- .and_then(|value| non_empty(value.as_str()))
- .or_else(|| {
- existing_document
- .and_then(|document| document.listing_defaults.location.country.as_ref())
- .and_then(|value| non_empty(value.as_str()))
- })
-}
-
-fn existing_geohash(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document
- .and_then(|document| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| non_empty(location.geohash.as_str()))
- })
- .or_else(|| {
- existing_document
- .and_then(|document| non_empty(document.listing_defaults.location.geohash.as_str()))
- })
-}
-
-fn existing_delivery_method(existing_document: Option<&FarmConfigDocument>) -> Option<String> {
- existing_document
- .and_then(|document| non_empty(document.listing_defaults.delivery_method.as_str()))
-}
-
-fn resolved_name(document: &FarmConfigDocument) -> Option<String> {
- non_empty(document.profile.name.as_str()).or_else(|| non_empty(document.farm.name.as_str()))
-}
-
-fn resolved_location_primary(document: &FarmConfigDocument) -> Option<String> {
- non_empty(document.listing_defaults.location.primary.as_str()).or_else(|| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| non_empty(location.primary.as_str()))
- })
-}
-
-fn resolved_delivery_method(document: &FarmConfigDocument) -> Option<String> {
- non_empty(document.listing_defaults.delivery_method.as_str())
-}
-
-fn display_delivery_method(value: &str) -> String {
- value
- .split('_')
- .filter(|segment| !segment.is_empty())
- .map(capitalize_ascii_word)
- .collect::<Vec<_>>()
- .join(" ")
-}
-
-fn capitalize_ascii_word(word: &str) -> String {
- let mut chars = word.chars();
- let Some(first) = chars.next() else {
- return String::new();
- };
- let mut rendered = String::new();
- rendered.push(first.to_ascii_uppercase());
- rendered.push_str(chars.as_str());
- rendered
-}
-
-fn non_empty(value: &str) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_owned())
- }
-}
-
-fn now_unix() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .unwrap_or_default()
-}
-
-fn generate_d_tag() -> String {
- let nanos = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_nanos())
- .unwrap_or_default();
- let counter = D_TAG_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
- encode_base64url_no_pad((nanos ^ counter).to_be_bytes())
-}
-
-fn encode_base64url_no_pad(bytes: [u8; 16]) -> String {
- const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
- let mut output = String::with_capacity(22);
- let mut index = 0usize;
- while index + 3 <= bytes.len() {
- let block = ((bytes[index] as u32) << 16)
- | ((bytes[index + 1] as u32) << 8)
- | (bytes[index + 2] as u32);
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
- output.push(ALPHABET[(block & 0x3f) as usize] as char);
- index += 3;
- }
- let remaining = bytes.len() - index;
- if remaining == 1 {
- let block = (bytes[index] as u32) << 16;
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- } else if remaining == 2 {
- let block = ((bytes[index] as u32) << 16) | ((bytes[index + 1] as u32) << 8);
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
- }
- output
-}
-
-#[cfg(test)]
-mod tests {
- use super::{authored_profile_from_draft, generate_d_tag, require_verified_publish_media};
- use crate::runtime::RuntimeError;
- use crate::runtime::farm_config::FarmProfileDraft;
- use radroots_event::farm::Farm;
- use radroots_event_codec::d_tag::is_d_tag_base64url;
- use radroots_event_codec::profile::authored::authored_profile_to_wire_parts;
- use serde_json::json;
-
- #[test]
- fn generated_farm_d_tag_is_valid_base64url() {
- assert!(is_d_tag_base64url(&generate_d_tag()));
- }
-
- #[test]
- fn farm_profile_draft_uses_strict_tagless_profile_authoring() {
- let draft = FarmProfileDraft {
- name: "moss street farm".to_owned(),
- display_name: Some("Moss Street Farm".to_owned()),
- nip05: Some("farm@example.com".to_owned()),
- about: Some("Victoria produce".to_owned()),
- website: Some("https://example.com".to_owned()),
- bot: Some("true".to_owned()),
- ..FarmProfileDraft::default()
- };
-
- let authored = authored_profile_from_draft(&draft).expect("strict authored Profile");
- let wire = authored_profile_to_wire_parts(&authored).expect("Profile wire parts");
-
- assert!(wire.tags.is_empty());
- assert_eq!(
- serde_json::from_str::<serde_json::Value>(&wire.content).expect("Profile metadata"),
- json!({
- "name": "moss street farm",
- "display_name": "Moss Street Farm",
- "about": "Victoria produce",
- "nip05": "farm@example.com",
- "bot": true,
- })
- );
- }
-
- #[test]
- fn farm_profile_draft_rejects_non_boolean_legacy_bot_values() {
- let draft = FarmProfileDraft {
- name: "moss street farm".to_owned(),
- bot: Some("yes".to_owned()),
- ..FarmProfileDraft::default()
- };
-
- let RuntimeError::Config(message) =
- authored_profile_from_draft(&draft).expect_err("invalid bot must fail closed")
- else {
- panic!("expected config error");
- };
- assert_eq!(message, "farm profile bot must be `true` or `false`");
- }
-
- #[test]
- fn farm_publish_rejects_raw_media_without_blossom_proofs() {
- let mut draft = FarmProfileDraft {
- name: "moss street farm".to_owned(),
- picture: Some("https://media.example/picture.jpg".to_owned()),
- ..FarmProfileDraft::default()
- };
- let mut farm = sample_farm();
-
- assert_blossom_proof_error(require_verified_publish_media(&draft, &farm));
-
- draft.picture = None;
- farm.banner = Some("https://media.example/banner.jpg".to_owned());
- assert_blossom_proof_error(require_verified_publish_media(&draft, &farm));
- }
-
- fn sample_farm() -> Farm {
- Farm {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- name: "Moss Street Farm".to_owned(),
- about: None,
- website: None,
- picture: None,
- banner: None,
- location: None,
- tags: None,
- }
- }
-
- fn assert_blossom_proof_error(result: Result<(), RuntimeError>) {
- let RuntimeError::Config(message) = result.expect_err("raw media must fail closed") else {
- panic!("expected config error");
- };
- assert!(message.contains("Blossom descriptor"));
- assert!(message.contains("BUD-02 upload completion"));
- }
-}
diff --git a/src/runtime/farm_config.rs b/src/runtime/farm_config.rs
@@ -1,623 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-
-use radroots_event::farm::Farm;
-use radroots_event::listing::operational::{
- OperationalListingDeliveryMethod, OperationalListingPublicLocation,
-};
-use radroots_event_codec::d_tag::is_d_tag_base64url;
-use serde::{Deserialize, Serialize};
-
-use crate::runtime::RuntimeError;
-use crate::runtime::config::{PathsConfig, RuntimeConfig};
-
-const FARM_CONFIG_FILE_NAME: &str = "farm.toml";
-pub const SUPPORTED_FARM_CONFIG_VERSION: u32 = 1;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum FarmConfigScope {
- User,
- Workspace,
-}
-
-impl FarmConfigScope {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::User => "user",
- Self::Workspace => "workspace",
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct FarmConfigDocument {
- pub version: u32,
- pub selection: FarmConfigSelection,
- pub profile: FarmProfileDraft,
- pub farm: Farm,
- pub listing_defaults: FarmListingDefaults,
- #[serde(default)]
- pub publication: FarmPublicationStatus,
-}
-
-/// CLI-owned local draft for a future kind-0 Profile replacement.
-///
-/// String media references are deliberately retained only as local draft data.
-/// They are not proof of Blossom byte verification or BUD-02 upload completion
-/// and therefore cannot cross the Profile publication boundary.
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct FarmProfileDraft {
- pub name: String,
- pub display_name: Option<String>,
- pub nip05: Option<String>,
- pub about: Option<String>,
- pub website: Option<String>,
- pub picture: Option<String>,
- pub banner: Option<String>,
- pub lud06: Option<String>,
- pub lud16: Option<String>,
- pub bot: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct FarmConfigSelection {
- pub scope: FarmConfigScope,
- pub account: String,
- pub farm_d_tag: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct FarmListingDefaults {
- pub delivery_method: String,
- pub location: OperationalListingPublicLocation,
-}
-
-impl FarmListingDefaults {
- pub fn delivery_method_model(&self) -> Result<OperationalListingDeliveryMethod, RuntimeError> {
- parse_delivery_method(self.delivery_method.as_str())
- }
-}
-
-#[derive(Debug, Clone, Default, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct FarmPublicationStatus {
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub profile_event_id: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub farm_event_id: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub profile_published_at: Option<u64>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- pub farm_published_at: Option<u64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct ResolvedFarmConfig {
- pub scope: FarmConfigScope,
- pub path: PathBuf,
- pub document: FarmConfigDocument,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FarmMissingField {
- Name,
- Location,
- City,
- Delivery,
- Country,
- Geohash,
-}
-
-impl FarmMissingField {
- pub fn label(self) -> &'static str {
- match self {
- Self::Name => "Farm name",
- Self::Location => "Location",
- Self::City => "City",
- Self::Delivery => "Delivery method",
- Self::Country => "Country",
- Self::Geohash => "Geohash",
- }
- }
-}
-
-pub fn resolve_scope(
- paths: &PathsConfig,
- explicit_scope: Option<FarmConfigScope>,
-) -> Result<FarmConfigScope, RuntimeError> {
- if let Some(scope) = explicit_scope {
- return Ok(scope);
- }
- match paths.profile.as_str() {
- "repo_local" => Ok(FarmConfigScope::Workspace),
- "interactive_user" => Ok(FarmConfigScope::User),
- other => Err(RuntimeError::Config(format!(
- "unsupported farm config path profile `{other}`"
- ))),
- }
-}
-
-pub fn user_config_path(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> {
- let Some(parent) = paths.app_config_path.parent() else {
- return Err(RuntimeError::Config(format!(
- "app config path {} has no parent directory",
- paths.app_config_path.display()
- )));
- };
- Ok(parent.join(FARM_CONFIG_FILE_NAME))
-}
-
-pub fn workspace_config_path(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> {
- let Some(path) = paths.workspace_config_path.as_ref() else {
- return Err(RuntimeError::Config(format!(
- "workspace farm config requires repo_local path profile, got `{}`",
- paths.profile
- )));
- };
- let Some(parent) = path.parent() else {
- return Err(RuntimeError::Config(format!(
- "workspace config path {} has no parent directory",
- path.display()
- )));
- };
- Ok(parent.join("config/apps/cli").join(FARM_CONFIG_FILE_NAME))
-}
-
-pub fn config_path(paths: &PathsConfig, scope: FarmConfigScope) -> Result<PathBuf, RuntimeError> {
- match scope {
- FarmConfigScope::User => user_config_path(paths),
- FarmConfigScope::Workspace => workspace_config_path(paths),
- }
-}
-
-pub fn load(
- config: &RuntimeConfig,
- explicit_scope: Option<FarmConfigScope>,
-) -> Result<Option<ResolvedFarmConfig>, RuntimeError> {
- load_from_paths(&config.paths, explicit_scope)
-}
-
-pub fn load_from_paths(
- paths: &PathsConfig,
- explicit_scope: Option<FarmConfigScope>,
-) -> Result<Option<ResolvedFarmConfig>, RuntimeError> {
- let scope = resolve_scope(paths, explicit_scope)?;
- let path = config_path(paths, scope)?;
- load_from_path(path.as_path(), scope)
-}
-
-pub fn load_from_path(
- path: &Path,
- scope: FarmConfigScope,
-) -> Result<Option<ResolvedFarmConfig>, RuntimeError> {
- if !path.exists() {
- return Ok(None);
- }
- let contents = fs::read_to_string(path)?;
- let document: FarmConfigDocument = toml::from_str(contents.as_str()).map_err(|error| {
- RuntimeError::Config(format!("parse farm config {}: {error}", path.display()))
- })?;
- validate(&document, scope)?;
- Ok(Some(ResolvedFarmConfig {
- scope,
- path: path.to_path_buf(),
- document,
- }))
-}
-
-pub fn write(
- paths: &PathsConfig,
- scope: FarmConfigScope,
- document: &FarmConfigDocument,
-) -> Result<PathBuf, RuntimeError> {
- validate(document, scope)?;
- let path = config_path(paths, scope)?;
- let Some(parent) = path.parent() else {
- return Err(RuntimeError::Config(format!(
- "farm config path {} has no parent directory",
- path.display()
- )));
- };
- fs::create_dir_all(parent)?;
- let encoded = toml::to_string_pretty(document).map_err(|error| {
- RuntimeError::Config(format!("encode farm config {}: {error}", path.display()))
- })?;
- fs::write(&path, encoded)?;
- Ok(path)
-}
-
-pub fn validate(
- document: &FarmConfigDocument,
- resolved_scope: FarmConfigScope,
-) -> Result<(), RuntimeError> {
- if document.version != SUPPORTED_FARM_CONFIG_VERSION {
- return Err(RuntimeError::Config(format!(
- "farm config version must be {}, got {}",
- SUPPORTED_FARM_CONFIG_VERSION, document.version
- )));
- }
- if document.selection.scope != resolved_scope {
- return Err(RuntimeError::Config(format!(
- "farm config scope `{}` does not match resolved `{}` scope",
- document.selection.scope.as_str(),
- resolved_scope.as_str()
- )));
- }
- if trimmed(document.selection.account.as_str()).is_empty() {
- return Err(RuntimeError::Config(
- "farm config selection.account must not be empty".to_owned(),
- ));
- }
- if trimmed(document.selection.farm_d_tag.as_str()).is_empty() {
- return Err(RuntimeError::Config(
- "farm config selection.farm_d_tag must not be empty".to_owned(),
- ));
- }
- if !is_d_tag_base64url(trimmed(document.selection.farm_d_tag.as_str())) {
- return Err(RuntimeError::Config(
- "farm config selection.farm_d_tag must be a 22-character base64url identifier"
- .to_owned(),
- ));
- }
- if trimmed(document.farm.d_tag.as_str()).is_empty() {
- return Err(RuntimeError::Config(
- "farm config farm.d_tag must not be empty".to_owned(),
- ));
- }
- if !is_d_tag_base64url(trimmed(document.farm.d_tag.as_str())) {
- return Err(RuntimeError::Config(
- "farm config farm.d_tag must be a 22-character base64url identifier".to_owned(),
- ));
- }
- if trimmed(document.selection.farm_d_tag.as_str()) != trimmed(document.farm.d_tag.as_str()) {
- return Err(RuntimeError::Config(
- "farm config selection.farm_d_tag must match farm.d_tag".to_owned(),
- ));
- }
- if !trimmed(document.listing_defaults.delivery_method.as_str()).is_empty() {
- let _ = document.listing_defaults.delivery_method_model()?;
- }
- Ok(())
-}
-
-pub fn missing_fields(document: &FarmConfigDocument) -> Vec<FarmMissingField> {
- let mut missing = Vec::new();
-
- if farm_name(document).is_none() {
- missing.push(FarmMissingField::Name);
- }
-
- let location_present = location_primary(document).is_some();
- if !location_present {
- missing.push(FarmMissingField::Location);
- }
- if location_present && location_city(document).is_none() {
- missing.push(FarmMissingField::City);
- }
-
- if trimmed(document.listing_defaults.delivery_method.as_str()).is_empty() {
- missing.push(FarmMissingField::Delivery);
- }
-
- if location_present && location_country(document).is_none() {
- missing.push(FarmMissingField::Country);
- }
- if location_present && location_geohash(document).is_none() {
- missing.push(FarmMissingField::Geohash);
- }
-
- missing
-}
-
-fn farm_name(document: &FarmConfigDocument) -> Option<&str> {
- non_empty_ref(document.profile.name.as_str())
- .or_else(|| non_empty_ref(document.farm.name.as_str()))
-}
-
-fn location_primary(document: &FarmConfigDocument) -> Option<&str> {
- non_empty_ref(document.listing_defaults.location.primary.as_str()).or_else(|| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| non_empty_ref(location.primary.as_str()))
- })
-}
-
-fn location_country(document: &FarmConfigDocument) -> Option<&str> {
- document
- .listing_defaults
- .location
- .country
- .as_deref()
- .and_then(non_empty_ref)
- .or_else(|| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| location.country.as_deref())
- .and_then(non_empty_ref)
- })
-}
-
-fn location_city(document: &FarmConfigDocument) -> Option<&str> {
- document
- .listing_defaults
- .location
- .city
- .as_deref()
- .and_then(non_empty_ref)
- .or_else(|| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| location.city.as_deref())
- .and_then(non_empty_ref)
- })
-}
-
-fn location_geohash(document: &FarmConfigDocument) -> Option<&str> {
- non_empty_ref(document.listing_defaults.location.geohash.as_str()).or_else(|| {
- document
- .farm
- .location
- .as_ref()
- .and_then(|location| non_empty_ref(location.geohash.as_str()))
- })
-}
-
-fn parse_delivery_method(value: &str) -> Result<OperationalListingDeliveryMethod, RuntimeError> {
- let method = trimmed(value);
- if method.is_empty() {
- return Err(RuntimeError::Config(
- "farm config listing_defaults.delivery_method must not be empty".to_owned(),
- ));
- }
- Ok(match method {
- "pickup" => OperationalListingDeliveryMethod::Pickup,
- "local_delivery" => OperationalListingDeliveryMethod::LocalDelivery,
- "shipping" => OperationalListingDeliveryMethod::Shipping,
- other => OperationalListingDeliveryMethod::Other {
- method: other.to_owned(),
- },
- })
-}
-
-fn trimmed(value: &str) -> &str {
- value.trim()
-}
-
-fn non_empty_ref(value: &str) -> Option<&str> {
- let trimmed = trimmed(value);
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- use std::path::PathBuf;
-
- use radroots_event::farm::FarmPublicLocation;
- use tempfile::tempdir;
-
- fn sample_paths(profile: &str, root: &Path) -> PathsConfig {
- let repo_local_root = root.join("infra/local/runtime/radroots");
- PathsConfig {
- profile: profile.to_owned(),
- profile_source: "test".to_owned(),
- allowed_profiles: vec!["interactive_user".to_owned(), "repo_local".to_owned()],
- root_source: "test".to_owned(),
- repo_local_root: Some(repo_local_root.clone()),
- repo_local_root_source: Some("test".to_owned()),
- subordinate_path_override_source: "test".to_owned(),
- app_namespace: "apps/cli".to_owned(),
- shared_accounts_namespace: "shared/accounts".to_owned(),
- shared_identities_namespace: "shared/identities".to_owned(),
- app_config_path: root.join("home/.radroots/config/apps/cli/config.toml"),
- workspace_config_path: (profile == "repo_local")
- .then(|| repo_local_root.join("config.toml")),
- app_data_root: root.join("home/.radroots/data/apps/cli"),
- shared_cache_root: root.join("home/.radroots/cache"),
- app_logs_root: root.join("home/.radroots/logs/apps/cli"),
- shared_accounts_data_root: root.join("home/.radroots/data/shared/accounts"),
- shared_accounts_secrets_root: root.join("home/.radroots/secrets/shared/accounts"),
- default_identity_path: root
- .join("home/.radroots/secrets/shared/identities/default.json"),
- }
- }
-
- fn sample_document(scope: FarmConfigScope) -> FarmConfigDocument {
- FarmConfigDocument {
- version: SUPPORTED_FARM_CONFIG_VERSION,
- selection: FarmConfigSelection {
- scope,
- account: "seller".to_owned(),
- farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- },
- profile: FarmProfileDraft {
- name: "La Huerta".to_owned(),
- display_name: Some("La Huerta".to_owned()),
- nip05: None,
- about: Some("Small mixed vegetable farm.".to_owned()),
- website: Some("https://example.invalid/la-huerta".to_owned()),
- picture: None,
- banner: None,
- lud06: None,
- lud16: None,
- bot: None,
- },
- farm: Farm {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- name: "La Huerta".to_owned(),
- about: Some("Small mixed vegetable farm.".to_owned()),
- website: Some("https://example.invalid/la-huerta".to_owned()),
- picture: None,
- banner: None,
- location: Some(FarmPublicLocation {
- primary: "San Francisco, CA".to_owned(),
- city: Some("San Francisco".to_owned()),
- region: Some("CA".to_owned()),
- country: Some("US".to_owned()),
- geohash: "9q8yy".to_owned(),
- }),
- tags: None,
- },
- listing_defaults: FarmListingDefaults {
- delivery_method: "pickup".to_owned(),
- location: OperationalListingPublicLocation {
- primary: "San Francisco, CA".to_owned(),
- city: Some("San Francisco".to_owned()),
- region: Some("CA".to_owned()),
- country: Some("US".to_owned()),
- geohash: "9q8yy".to_owned(),
- },
- },
- publication: FarmPublicationStatus::default(),
- }
- }
-
- #[test]
- fn resolve_scope_defaults_from_runtime_profile() {
- let dir = tempdir().expect("tempdir");
- let interactive_paths = sample_paths("interactive_user", dir.path());
- let repo_local_paths = sample_paths("repo_local", dir.path());
-
- assert_eq!(
- resolve_scope(&interactive_paths, None).expect("interactive scope"),
- FarmConfigScope::User
- );
- assert_eq!(
- resolve_scope(&repo_local_paths, None).expect("repo_local scope"),
- FarmConfigScope::Workspace
- );
- }
-
- #[test]
- fn explicit_scope_override_selects_requested_document() {
- let dir = tempdir().expect("tempdir");
- let paths = sample_paths("repo_local", dir.path());
- let document = sample_document(FarmConfigScope::User);
- let path = write(&paths, FarmConfigScope::User, &document).expect("write user farm config");
-
- let resolved =
- load_from_paths(&paths, Some(FarmConfigScope::User)).expect("load user farm config");
- let resolved = resolved.expect("resolved farm config");
-
- assert_eq!(resolved.scope, FarmConfigScope::User);
- assert_eq!(resolved.path, path);
- assert_eq!(resolved.document.selection.account, "seller");
- assert_eq!(resolved.document.selection.scope, FarmConfigScope::User);
- }
-
- #[test]
- fn write_and_load_workspace_config_round_trip() {
- let dir = tempdir().expect("tempdir");
- let paths = sample_paths("repo_local", dir.path());
- let document = sample_document(FarmConfigScope::Workspace);
- let expected_path = PathBuf::from(dir.path())
- .join("infra/local/runtime/radroots/config/apps/cli/farm.toml");
-
- let written_path =
- write(&paths, FarmConfigScope::Workspace, &document).expect("write workspace config");
- let resolved = load_from_paths(&paths, None).expect("load workspace config");
- let resolved = resolved.expect("resolved farm config");
-
- assert_eq!(written_path, expected_path);
- assert_eq!(resolved.path, expected_path);
- assert_eq!(resolved.scope, FarmConfigScope::Workspace);
- assert_eq!(
- resolved.document.selection.scope,
- FarmConfigScope::Workspace
- );
- assert_eq!(
- resolved.document.selection.farm_d_tag,
- "AAAAAAAAAAAAAAAAAAAAAA"
- );
- assert_eq!(resolved.document.farm.d_tag, "AAAAAAAAAAAAAAAAAAAAAA");
- assert_eq!(
- resolved.document.profile.website.as_deref(),
- Some("https://example.invalid/la-huerta")
- );
- assert_eq!(
- resolved.document.listing_defaults.location.primary,
- "San Francisco, CA"
- );
- }
-
- #[test]
- fn workspace_config_write_requires_repo_local_profile() {
- let dir = tempdir().expect("tempdir");
- let paths = sample_paths("interactive_user", dir.path());
- let document = sample_document(FarmConfigScope::Workspace);
- let repo_local_root = dir.path().join("infra/local/runtime/radroots");
-
- let error = write(&paths, FarmConfigScope::Workspace, &document)
- .expect_err("interactive workspace farm config should fail");
-
- match error {
- RuntimeError::Config(message) => {
- assert!(message.contains("requires repo_local path profile"));
- assert!(message.contains("interactive_user"));
- }
- other => panic!("expected config error, got {other:?}"),
- }
- assert!(!repo_local_root.exists());
- }
-
- #[test]
- fn load_rejects_scope_mismatch() {
- let dir = tempdir().expect("tempdir");
- let paths = sample_paths("repo_local", dir.path());
- let path = workspace_config_path(&paths).expect("workspace farm path");
- let Some(parent) = path.parent() else {
- panic!("workspace farm path should have parent");
- };
- fs::create_dir_all(parent).expect("create workspace farm config dir");
- let contents = toml::to_string_pretty(&sample_document(FarmConfigScope::User))
- .expect("encode mismatched farm config");
- fs::write(&path, contents).expect("write mismatched farm config");
-
- let error = load_from_paths(&paths, None).expect_err("scope mismatch should fail");
- match error {
- RuntimeError::Config(message) => {
- assert!(message.contains("does not match resolved `workspace` scope"));
- }
- other => panic!("expected config error, got {other:?}"),
- }
- }
-
- #[test]
- fn load_rejects_unsupported_version() {
- let dir = tempdir().expect("tempdir");
- let paths = sample_paths("interactive_user", dir.path());
- let path = user_config_path(&paths).expect("user farm path");
- let Some(parent) = path.parent() else {
- panic!("user farm path should have parent");
- };
- fs::create_dir_all(parent).expect("create user farm config dir");
- let mut document = sample_document(FarmConfigScope::User);
- document.version = 2;
- let contents = toml::to_string_pretty(&document).expect("encode version mismatch");
- fs::write(&path, contents).expect("write version mismatch config");
-
- let error = load_from_paths(&paths, None).expect_err("version mismatch should fail");
- match error {
- RuntimeError::Config(message) => {
- assert!(message.contains("farm config version must be 1, got 2"));
- }
- other => panic!("expected config error, got {other:?}"),
- }
- }
-}
diff --git a/src/runtime/find.rs b/src/runtime/find.rs
@@ -1,235 +0,0 @@
-use radroots_replica_store::ReplicaSql;
-use radroots_sql_core::SqlxSqliteExecutor;
-
-use crate::cli::global::FindQueryArgs;
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::hyf::{self, HyfQueryRewriteRequest, HyfRequestContext};
-use crate::runtime::sync::{
- RelayIngestScope, freshness_for_scope_from_executor, freshness_requires_refresh,
- market_refresh, missing_freshness,
-};
-use crate::view::runtime::{
- FindHyfView, FindPriceView, FindQuantityView, FindResultHyfView, FindResultProvenanceView,
- FindResultView, FindView, MarketReadinessView,
-};
-
-const FIND_SOURCE: &str = "local replica · local first";
-const FIND_HYF_SOURCE: &str = "hyf query_rewrite · local first";
-const FIND_HYF_QUERY_REWRITE_REQUEST_ID: &str = "cli-find-query-rewrite";
-
-#[derive(Debug, Clone)]
-struct AppliedQueryRewrite {
- rewritten_query: String,
- query_terms: Vec<String>,
-}
-
-impl AppliedQueryRewrite {
- fn to_find_view(&self) -> FindHyfView {
- FindHyfView {
- state: "query_rewrite_applied".to_owned(),
- source: FIND_HYF_SOURCE.to_owned(),
- rewritten_query: self.rewritten_query.clone(),
- query_terms: self.query_terms.clone(),
- }
- }
-
- fn to_result_view(&self) -> FindResultHyfView {
- FindResultHyfView {
- state: "query_rewrite_applied".to_owned(),
- rewritten_query: self.rewritten_query.clone(),
- query_terms: self.query_terms.clone(),
- }
- }
-}
-
-pub fn search(config: &RuntimeConfig, args: &FindQueryArgs) -> Result<FindView, RuntimeError> {
- let query = args.query.join(" ");
- if !config.local.replica_store_path.exists() {
- return Ok(FindView {
- state: "unconfigured".to_owned(),
- source: FIND_SOURCE.to_owned(),
- query,
- count: 0,
- relay_count: config.transport.nostr_relay_urls.len(),
- replica_store: config.local.replica_store_path.display().to_string(),
- freshness: missing_freshness(),
- results: Vec::new(),
- hyf: None,
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store inspect".to_owned()],
- });
- }
-
- refresh_market_if_needed(config)?;
- let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?);
- let freshness =
- freshness_for_scope_from_executor(config, db.executor(), RelayIngestScope::MarketPull)?;
- let applied_query_rewrite = attempt_query_rewrite(config, query.as_str(), &args.query);
- let effective_query_terms = applied_query_rewrite
- .as_ref()
- .map(|rewrite| rewrite.query_terms.clone())
- .unwrap_or_else(|| normalize_query_terms(args.query.clone()));
- let rows = db.trade_product_search(effective_query_terms.as_slice())?;
- let relay_count = config.transport.nostr_relay_urls.len();
- let result_provenance = FindResultProvenanceView {
- origin: "local_replica.trade_product".to_owned(),
- freshness: freshness.display.clone(),
- relay_count,
- };
- let results = rows
- .into_iter()
- .map(|row| {
- let listing_addr = row.listing_addr.and_then(non_empty);
- let primary_bin_id = row.primary_bin_id.and_then(non_empty);
- let verified_primary_bin_id = row.verified_primary_bin_id.and_then(non_empty);
- let available_amount = row.qty_avail;
- let price_amount = row.price_amt;
- let price_currency = row.price_currency;
- let price_per_amount = row.price_qty_amt;
- let readiness = MarketReadinessView::from_market_projection(
- listing_addr.as_deref(),
- primary_bin_id.as_deref(),
- verified_primary_bin_id.as_deref(),
- Some(row.title.as_str()),
- Some(row.category.as_str()),
- available_amount,
- price_amount,
- price_currency.as_str(),
- price_per_amount,
- );
- FindResultView {
- id: row.id,
- product_key: row.key,
- readiness,
- listing_addr,
- primary_bin_id,
- title: row.title,
- category: row.category,
- summary: non_empty(row.summary),
- location_primary: row.location_primary.and_then(non_empty),
- available: FindQuantityView {
- total_amount: row.qty_amt,
- total_unit: row.qty_unit,
- label: row.qty_label.and_then(non_empty),
- available_amount,
- },
- price: FindPriceView {
- amount: price_amount,
- currency: price_currency,
- per_amount: price_per_amount,
- per_unit: row.price_qty_unit,
- },
- provenance: result_provenance.clone(),
- hyf: applied_query_rewrite
- .as_ref()
- .map(AppliedQueryRewrite::to_result_view),
- }
- })
- .collect::<Vec<_>>();
-
- let (state, reason, actions) = if results.is_empty() {
- let actions = if freshness.state == "never" {
- vec!["radroots sync status".to_owned()]
- } else {
- Vec::new()
- };
- (
- "empty".to_owned(),
- Some(format!("no local market results matched `{query}`")),
- actions,
- )
- } else {
- ("ready".to_owned(), None, Vec::new())
- };
-
- Ok(FindView {
- state,
- source: FIND_SOURCE.to_owned(),
- query,
- count: results.len(),
- relay_count,
- replica_store: config.local.replica_store_path.display().to_string(),
- freshness,
- results,
- hyf: applied_query_rewrite.map(|rewrite| rewrite.to_find_view()),
- reason,
- actions,
- })
-}
-
-fn refresh_market_if_needed(config: &RuntimeConfig) -> Result<(), RuntimeError> {
- if config.output.dry_run || config.transport.nostr_relay_urls.is_empty() {
- return Ok(());
- }
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- let freshness =
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?;
- if freshness_requires_refresh(&freshness) {
- let _ = market_refresh(config)?;
- }
- Ok(())
-}
-
-fn attempt_query_rewrite(
- config: &RuntimeConfig,
- query: &str,
- original_terms: &[String],
-) -> Option<AppliedQueryRewrite> {
- if query.trim().is_empty() {
- return None;
- }
-
- let client = hyf::resolve_runtime_client(config).ok()?;
- let response = client
- .query_rewrite(
- FIND_HYF_QUERY_REWRITE_REQUEST_ID,
- Some(FIND_HYF_QUERY_REWRITE_REQUEST_ID),
- &HyfRequestContext::deterministic_cli(),
- &HyfQueryRewriteRequest::new(query),
- )
- .ok()?;
-
- let rewritten_terms = normalize_query_terms(response.output.query_terms.clone());
- if rewritten_terms.is_empty() {
- return None;
- }
-
- if rewritten_terms == normalize_query_terms(original_terms.iter().cloned()) {
- return None;
- }
-
- let rewritten_query = {
- let rewritten_text = response.output.rewritten_text.trim();
- if rewritten_text.is_empty() {
- rewritten_terms.join(" ")
- } else {
- rewritten_text.to_owned()
- }
- };
-
- Some(AppliedQueryRewrite {
- rewritten_query,
- query_terms: rewritten_terms,
- })
-}
-
-fn non_empty(value: String) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_owned())
- }
-}
-
-fn normalize_query_terms<I>(terms: I) -> Vec<String>
-where
- I: IntoIterator<Item = String>,
-{
- terms
- .into_iter()
- .map(|term| term.trim().to_lowercase())
- .filter(|term| !term.is_empty())
- .collect()
-}
diff --git a/src/runtime/hyf.rs b/src/runtime/hyf.rs
@@ -1,1118 +0,0 @@
-use std::collections::BTreeMap;
-use std::io::{Read, Write};
-use std::path::{Path, PathBuf};
-use std::process::{Child, Command, ExitStatus, Output, Stdio};
-use std::thread;
-use std::time::{Duration, Instant};
-
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use crate::runtime::config::{
- CapabilityBindingTargetKind, HyfConfig, INFERENCE_HYF_STDIO_CAPABILITY, RuntimeConfig,
-};
-
-const HYF_CONTROL_TIMEOUT: Duration = Duration::from_secs(2);
-const HYF_BUSINESS_TIMEOUT: Duration = Duration::from_secs(4);
-const HYF_TIMEOUT_POLL_INTERVAL: Duration = Duration::from_millis(10);
-const HYF_STATUS_REQUEST_ID: &str = "cli-doctor-hyf-status";
-const HYF_CAPABILITIES_REQUEST_ID: &str = "cli-runtime-hyf-capabilities";
-const HYF_SOURCE: &str = "hyf status control request · local first";
-const HYF_PROTOCOL_VERSION: u64 = 1;
-const HYF_CONSUMER: &str = "radroots-cli";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HyfStatusView {
- pub executable: String,
- pub state: String,
- pub source: String,
- pub reason: Option<String>,
- pub protocol_version: Option<u64>,
- pub deterministic_available: Option<bool>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HyfClient {
- executable: PathBuf,
- timeouts: HyfClientTimeouts,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct HyfClientTimeouts {
- control: Duration,
- business: Duration,
-}
-
-impl Default for HyfClientTimeouts {
- fn default() -> Self {
- Self {
- control: HYF_CONTROL_TIMEOUT,
- business: HYF_BUSINESS_TIMEOUT,
- }
- }
-}
-
-impl HyfClient {
- pub fn new(executable: PathBuf) -> Self {
- Self {
- executable,
- timeouts: HyfClientTimeouts::default(),
- }
- }
-
- #[cfg(test)]
- fn with_timeouts(executable: PathBuf, control: Duration, business: Duration) -> Self {
- Self {
- executable,
- timeouts: HyfClientTimeouts { control, business },
- }
- }
-
- pub fn executable(&self) -> &Path {
- self.executable.as_path()
- }
-
- pub fn status(&self) -> Result<HyfSuccess<HyfStatusOutput>, HyfClientError> {
- self.call(
- HYF_STATUS_REQUEST_ID,
- Some(HYF_STATUS_REQUEST_ID),
- "sys.status",
- None,
- &HyfEmptyInput::default(),
- self.timeouts.control,
- )
- }
-
- pub fn capabilities(&self) -> Result<HyfSuccess<HyfCapabilitiesOutput>, HyfClientError> {
- self.call(
- HYF_CAPABILITIES_REQUEST_ID,
- None,
- "sys.capabilities",
- None,
- &HyfEmptyInput::default(),
- self.timeouts.control,
- )
- }
-
- pub fn query_rewrite(
- &self,
- request_id: &str,
- trace_id: Option<&str>,
- context: &HyfRequestContext,
- request: &HyfQueryRewriteRequest,
- ) -> Result<HyfSuccess<HyfQueryRewriteOutput>, HyfClientError> {
- self.call(
- request_id,
- trace_id,
- "query_rewrite",
- Some(context),
- request,
- self.timeouts.business,
- )
- }
-
- pub fn semantic_rank(
- &self,
- request_id: &str,
- trace_id: Option<&str>,
- context: &HyfRequestContext,
- request: &HyfSemanticRankRequest,
- ) -> Result<HyfSuccess<HyfSemanticRankOutput>, HyfClientError> {
- self.call(
- request_id,
- trace_id,
- "semantic_rank",
- Some(context),
- request,
- self.timeouts.business,
- )
- }
-
- pub fn explain_result(
- &self,
- request_id: &str,
- trace_id: Option<&str>,
- context: &HyfRequestContext,
- request: &HyfExplainResultRequest,
- ) -> Result<HyfSuccess<HyfExplainResultOutput>, HyfClientError> {
- self.call(
- request_id,
- trace_id,
- "explain_result",
- Some(context),
- request,
- self.timeouts.business,
- )
- }
-
- fn call<TRequest, TResponse>(
- &self,
- request_id: &str,
- trace_id: Option<&str>,
- capability: &str,
- context: Option<&HyfRequestContext>,
- input: &TRequest,
- timeout: Duration,
- ) -> Result<HyfSuccess<TResponse>, HyfClientError>
- where
- TRequest: Serialize,
- TResponse: for<'de> Deserialize<'de>,
- {
- let request = serialize_request(request_id, trace_id, capability, context, input)
- .map_err(HyfClientError::SerializeRequest)?;
-
- let output = self.run_request(request.as_str(), timeout)?;
- let stdout = String::from_utf8(output.stdout).map_err(HyfClientError::InvalidUtf8)?;
- let response: HyfWireResponse<TResponse> =
- serde_json::from_str(stdout.as_str()).map_err(HyfClientError::InvalidJson)?;
-
- if !response.ok {
- return Err(HyfClientError::RemoteError {
- code: response.error.as_ref().and_then(|error| error.code.clone()),
- message: response
- .error
- .as_ref()
- .and_then(|error| error.message.clone()),
- });
- }
-
- let Some(output) = response.output else {
- return Err(HyfClientError::InvalidResponse(
- "hyf response omitted output for a successful request".to_owned(),
- ));
- };
-
- Ok(HyfSuccess {
- version: response.version,
- request_id: response.request_id,
- trace_id: response.trace_id,
- output,
- meta: response.meta,
- })
- }
-
- fn run_request(&self, request: &str, timeout: Duration) -> Result<Output, HyfClientError> {
- let mut child = Command::new(&self.executable)
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .spawn()
- .map_err(|error| match error.kind() {
- std::io::ErrorKind::NotFound => HyfClientError::NotFound,
- _ => HyfClientError::Start(error),
- })?;
-
- if let Some(mut stdin) = child.stdin.take() {
- writeln!(stdin, "{request}").map_err(HyfClientError::Write)?;
- }
-
- let output = collect_output_with_timeout(child, timeout)?;
- if !output.status.success() {
- return Err(HyfClientError::NonZeroExit {
- status: output.status.code(),
- stderr: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
- });
- }
- Ok(output)
- }
-}
-
-fn serialize_request<TRequest: Serialize>(
- request_id: &str,
- trace_id: Option<&str>,
- capability: &str,
- context: Option<&HyfRequestContext>,
- input: &TRequest,
-) -> Result<String, serde_json::Error> {
- serde_json::to_string(&HyfRequestEnvelope {
- version: HYF_PROTOCOL_VERSION,
- request_id,
- trace_id,
- capability,
- context,
- input,
- })
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HyfSuccess<T> {
- pub version: u64,
- pub request_id: String,
- pub trace_id: Option<String>,
- pub output: T,
- pub meta: Option<Value>,
-}
-
-#[derive(Debug, thiserror::Error)]
-pub enum HyfClientError {
- #[error("hyf executable was not found")]
- NotFound,
- #[error("failed to start hyf request: {0}")]
- Start(std::io::Error),
- #[error("failed to write hyf request stdin: {0}")]
- Write(std::io::Error),
- #[error("failed to wait on hyf request: {0}")]
- Wait(std::io::Error),
- #[error("failed to read hyf request output: {0}")]
- Read(std::io::Error),
- #[error("hyf request timed out after {0}ms")]
- Timeout(u128),
- #[error("hyf request exited unsuccessfully")]
- NonZeroExit { status: Option<i32>, stderr: String },
- #[error("failed to serialize hyf request: {0}")]
- SerializeRequest(serde_json::Error),
- #[error("hyf response was not valid UTF-8: {0}")]
- InvalidUtf8(std::string::FromUtf8Error),
- #[error("hyf response was not valid JSON: {0}")]
- InvalidJson(serde_json::Error),
- #[error("{0}")]
- InvalidResponse(String),
- #[error("hyf request returned a remote error")]
- RemoteError {
- code: Option<String>,
- message: Option<String>,
- },
-}
-
-#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
-pub struct HyfRequestContext {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub consumer: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub execution_mode_preference: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub scope: Option<HyfRequestScope>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub return_provenance: Option<bool>,
-}
-
-impl HyfRequestContext {
- pub fn deterministic_cli() -> Self {
- Self {
- consumer: Some(HYF_CONSUMER.to_owned()),
- execution_mode_preference: Some("deterministic".to_owned()),
- scope: None,
- return_provenance: None,
- }
- }
-
- pub fn with_return_provenance(mut self, return_provenance: bool) -> Self {
- self.return_provenance = Some(return_provenance);
- self
- }
-
- pub fn with_listing_scope(mut self, listing_ids: Vec<String>) -> Self {
- self.scope = if listing_ids.is_empty() {
- None
- } else {
- Some(HyfRequestScope { listing_ids })
- };
- self
- }
-}
-
-#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
-pub struct HyfRequestScope {
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub listing_ids: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
-pub struct HyfQueryRewriteRequest {
- pub query: String,
-}
-
-impl HyfQueryRewriteRequest {
- pub fn new(query: impl Into<String>) -> Self {
- Self {
- query: query.into(),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct HyfSemanticCandidate {
- pub id: String,
- pub title: String,
- pub farm: String,
- pub delivery: String,
- pub distance_km: f64,
- pub freshness_minutes: i64,
-}
-
-#[derive(Debug, Clone, Serialize, PartialEq)]
-pub struct HyfSemanticRankRequest {
- pub query: String,
- pub candidates: Vec<HyfSemanticCandidate>,
-}
-
-impl HyfSemanticRankRequest {
- pub fn new(query: impl Into<String>, candidates: Vec<HyfSemanticCandidate>) -> Self {
- Self {
- query: query.into(),
- candidates,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, PartialEq)]
-pub struct HyfExplainResultRequest {
- pub query: String,
- pub candidate: HyfSemanticCandidate,
-}
-
-impl HyfExplainResultRequest {
- pub fn new(query: impl Into<String>, candidate: HyfSemanticCandidate) -> Self {
- Self {
- query: query.into(),
- candidate,
- }
- }
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfBuildIdentity {
- pub protocol_version: u64,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfExecutionModes {
- pub deterministic: bool,
- #[serde(default)]
- pub assisted: Option<bool>,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfStatusOutput {
- pub build_identity: HyfBuildIdentity,
- pub enabled_execution_modes: HyfExecutionModes,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfRequestContextContract {
- pub accepted_features: Vec<String>,
- pub effective_features: Vec<String>,
- pub unsupported_field_behavior: String,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfBusinessCapability {
- pub id: String,
- pub kind: String,
- pub deterministic_execution: String,
- pub implementation_status: String,
- pub callable: bool,
- pub implemented: bool,
- pub assisted_execution: String,
- pub assisted_backend_available: bool,
- #[serde(default)]
- pub disabled_reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq)]
-pub struct HyfCapabilitiesOutput {
- pub control_routes: Vec<String>,
- pub business_capabilities: Vec<HyfBusinessCapability>,
- pub assisted_backend_capabilities: Vec<Value>,
- pub request_context_contract: HyfRequestContextContract,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfExtractedFilters {
- pub local_intent: bool,
- pub fulfillment: String,
- pub time_window: String,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfQueryRewriteOutput {
- pub original_text: String,
- pub normalized_text: String,
- pub rewritten_text: String,
- pub query_terms: Vec<String>,
- pub normalization_signals: Vec<String>,
- pub ranking_hints: Vec<String>,
- pub extracted_filters: HyfExtractedFilters,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfScoredCandidate {
- pub id: String,
- pub heuristic_score: i64,
- pub matched_terms: Vec<String>,
- pub reasons: Vec<String>,
- pub delivery_alignment: String,
- pub distance_band: String,
- pub freshness_band: String,
- pub scope_match: bool,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfSemanticRankOutput {
- pub ranked_ids: Vec<String>,
- pub reasons: BTreeMap<String, Vec<String>>,
- pub scored_candidates: Vec<HyfScoredCandidate>,
- pub ranking_hints: Vec<String>,
- pub extracted_filters: HyfExtractedFilters,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfSignalAssessment {
- pub delivery_alignment: String,
- pub distance_band: String,
- pub freshness_band: String,
- pub scope_match: bool,
-}
-
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
-pub struct HyfExplainResultOutput {
- pub result_id: String,
- pub explanation_kind: String,
- pub summary: String,
- pub score: i64,
- pub reasons: Vec<String>,
- pub matched_terms: Vec<String>,
- pub ranking_hints: Vec<String>,
- pub extracted_filters: HyfExtractedFilters,
- pub signal_assessment: HyfSignalAssessment,
-}
-
-#[derive(Debug, Clone, Serialize, Default)]
-struct HyfEmptyInput {}
-
-#[derive(Debug, Serialize)]
-struct HyfRequestEnvelope<'a, T> {
- version: u64,
- request_id: &'a str,
- #[serde(skip_serializing_if = "Option::is_none")]
- trace_id: Option<&'a str>,
- capability: &'a str,
- #[serde(skip_serializing_if = "Option::is_none")]
- context: Option<&'a HyfRequestContext>,
- input: &'a T,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(bound(deserialize = "T: Deserialize<'de>"))]
-struct HyfWireResponse<T> {
- version: u64,
- request_id: String,
- #[serde(default)]
- trace_id: Option<String>,
- ok: bool,
- #[serde(default)]
- output: Option<T>,
- #[serde(default)]
- meta: Option<Value>,
- #[serde(default)]
- error: Option<HyfWireError>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-struct HyfWireError {
- #[serde(default)]
- code: Option<String>,
- #[serde(default)]
- message: Option<String>,
-}
-
-pub fn resolve_runtime_client(config: &RuntimeConfig) -> Result<HyfClient, HyfStatusView> {
- if !config.hyf.enabled {
- return Err(disabled_status(config.hyf.executable.display().to_string()));
- }
-
- let Some(binding) = config.capability_binding(INFERENCE_HYF_STDIO_CAPABILITY) else {
- return resolve_client(&config.hyf);
- };
-
- match binding.target_kind {
- CapabilityBindingTargetKind::ExplicitEndpoint => resolve_client(&HyfConfig {
- enabled: true,
- executable: binding.target.clone().into(),
- }),
- CapabilityBindingTargetKind::ManagedInstance => Err(unavailable_status(
- config.hyf.executable.display().to_string(),
- format!(
- "configured hyf binding target `{}` uses unsupported target_kind `managed_instance`; use `explicit_endpoint` for `inference.hyf_stdio`",
- binding.target
- ),
- None,
- None,
- )),
- }
-}
-
-pub fn resolve_runtime_status(config: &RuntimeConfig) -> HyfStatusView {
- match resolve_runtime_client(config) {
- Ok(client) => resolve_status_for_client(&client),
- Err(view) => view,
- }
-}
-
-pub fn resolve_status(config: &HyfConfig) -> HyfStatusView {
- match resolve_client(config) {
- Ok(client) => resolve_status_for_client(&client),
- Err(view) => view,
- }
-}
-
-fn resolve_client(config: &HyfConfig) -> Result<HyfClient, HyfStatusView> {
- let executable = config.executable.display().to_string();
- if !config.enabled {
- return Err(disabled_status(executable));
- }
-
- if config.executable.as_os_str().is_empty() {
- return Err(unavailable_status(
- executable,
- "hyf executable path is not configured".to_owned(),
- None,
- None,
- ));
- }
-
- Ok(HyfClient::new(config.executable.clone()))
-}
-
-fn resolve_status_for_client(client: &HyfClient) -> HyfStatusView {
- let executable = client.executable().display().to_string();
- let response = match client.status() {
- Ok(response) => response,
- Err(HyfClientError::NotFound) => {
- return unavailable_status(
- executable,
- format!(
- "hyf executable was not found at {}",
- client.executable().display()
- ),
- None,
- None,
- );
- }
- Err(HyfClientError::Start(error)) => {
- return unavailable_status(
- executable,
- format!(
- "failed to start hyf control request at {}: {error}",
- client.executable().display()
- ),
- None,
- None,
- );
- }
- Err(HyfClientError::Write(error)) => {
- return unavailable_status(
- executable,
- format!("failed to write hyf control request stdin: {error}"),
- None,
- None,
- );
- }
- Err(HyfClientError::Timeout(timeout_ms)) => {
- return unavailable_status(
- executable,
- format!("hyf status control request timed out after {timeout_ms}ms"),
- None,
- None,
- );
- }
- Err(HyfClientError::Wait(error)) | Err(HyfClientError::Read(error)) => {
- return unavailable_status(
- executable,
- format!("failed to capture hyf status control output: {error}"),
- None,
- None,
- );
- }
- Err(HyfClientError::NonZeroExit { status, stderr }) => {
- return unavailable_status(
- executable,
- format_nonzero_exit("hyf status control request", status, stderr.as_str()),
- None,
- None,
- );
- }
- Err(HyfClientError::InvalidUtf8(error)) => {
- return unavailable_status(
- executable,
- format!("hyf status output was not valid UTF-8: {error}"),
- None,
- None,
- );
- }
- Err(HyfClientError::InvalidJson(error)) => {
- return unavailable_status(
- executable,
- format!("hyf status output was not valid JSON: {error}"),
- None,
- None,
- );
- }
- Err(HyfClientError::RemoteError { code, .. }) => {
- let reason = code
- .map(|code| format!("hyf status control request returned error code {code}"))
- .unwrap_or_else(|| {
- "hyf status control request returned an invalid error response".to_owned()
- });
- return unavailable_status(executable, reason, None, None);
- }
- Err(HyfClientError::SerializeRequest(_) | HyfClientError::InvalidResponse(_)) => {
- return unavailable_status(
- executable,
- "hyf status control request returned an invalid error response".to_owned(),
- None,
- None,
- );
- }
- };
-
- let protocol_version = Some(response.output.build_identity.protocol_version);
- let deterministic_available = Some(response.output.enabled_execution_modes.deterministic);
-
- if response.version != HYF_PROTOCOL_VERSION {
- return unavailable_status(
- executable,
- format!(
- "hyf status response version {:?} is incompatible with cli expected {}",
- Some(response.version),
- HYF_PROTOCOL_VERSION
- ),
- protocol_version,
- deterministic_available,
- );
- }
-
- if response.request_id != HYF_STATUS_REQUEST_ID {
- return unavailable_status(
- executable,
- "hyf status response did not preserve the control request id".to_owned(),
- protocol_version,
- deterministic_available,
- );
- }
-
- if protocol_version != Some(HYF_PROTOCOL_VERSION) {
- return unavailable_status(
- executable,
- format!(
- "hyf protocol version {:?} is incompatible with cli expected {}",
- protocol_version, HYF_PROTOCOL_VERSION
- ),
- protocol_version,
- deterministic_available,
- );
- }
-
- if deterministic_available != Some(true) {
- return unavailable_status(
- executable,
- "hyf deterministic execution is unavailable".to_owned(),
- protocol_version,
- deterministic_available,
- );
- }
-
- HyfStatusView {
- executable,
- state: "ready".to_owned(),
- source: HYF_SOURCE.to_owned(),
- reason: Some("healthy · protocol 1 · deterministic available".to_owned()),
- protocol_version,
- deterministic_available,
- }
-}
-
-fn collect_output_with_timeout(
- mut child: Child,
- timeout: Duration,
-) -> Result<Output, HyfClientError> {
- let started_at = Instant::now();
- loop {
- match child.try_wait() {
- Ok(Some(status)) => return collect_output(child, status),
- Ok(None) => {
- if started_at.elapsed() >= timeout {
- let _ = child.kill();
- let _ = child.wait();
- return Err(HyfClientError::Timeout(timeout.as_millis()));
- }
- thread::sleep(HYF_TIMEOUT_POLL_INTERVAL);
- }
- Err(error) => {
- let _ = child.kill();
- let _ = child.wait();
- return Err(HyfClientError::Wait(error));
- }
- }
- }
-}
-
-fn collect_output(mut child: Child, status: ExitStatus) -> Result<Output, HyfClientError> {
- let mut stdout = Vec::new();
- let mut stderr = Vec::new();
-
- if let Some(mut pipe) = child.stdout.take() {
- pipe.read_to_end(&mut stdout)
- .map_err(HyfClientError::Read)?;
- }
- if let Some(mut pipe) = child.stderr.take() {
- pipe.read_to_end(&mut stderr)
- .map_err(HyfClientError::Read)?;
- }
-
- Ok(Output {
- status,
- stdout,
- stderr,
- })
-}
-
-fn disabled_status(executable: String) -> HyfStatusView {
- HyfStatusView {
- executable,
- state: "disabled".to_owned(),
- source: HYF_SOURCE.to_owned(),
- reason: Some("disabled by config".to_owned()),
- protocol_version: None,
- deterministic_available: None,
- }
-}
-
-fn unavailable_status(
- executable: String,
- reason: String,
- protocol_version: Option<u64>,
- deterministic_available: Option<bool>,
-) -> HyfStatusView {
- HyfStatusView {
- executable,
- state: "unavailable".to_owned(),
- source: HYF_SOURCE.to_owned(),
- reason: Some(reason),
- protocol_version,
- deterministic_available,
- }
-}
-
-fn format_nonzero_exit(request_label: &str, status: Option<i32>, stderr: &str) -> String {
- match status {
- Some(code) if stderr.is_empty() => {
- format!("{request_label} exited with status code {code}")
- }
- Some(code) => {
- format!("{request_label} exited with status code {code}: {stderr}")
- }
- None if stderr.is_empty() => format!("{request_label} terminated by signal"),
- None => format!("{request_label} terminated by signal: {stderr}"),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- HYF_PROTOCOL_VERSION, HyfClient, HyfClientError, HyfEmptyInput, HyfExplainResultRequest,
- HyfQueryRewriteRequest, HyfRequestContext, HyfSemanticCandidate, HyfSemanticRankRequest,
- resolve_status,
- };
- use crate::runtime::config::HyfConfig;
- use serde::Serialize;
- use serde_json::Value;
- use std::fs;
- use std::os::unix::fs::PermissionsExt;
- use std::path::{Path, PathBuf};
- use std::sync::{Mutex, MutexGuard, OnceLock};
- use std::time::Duration;
- use tempfile::tempdir;
-
- fn hyf_test_lock() -> &'static Mutex<()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(()))
- }
-
- fn lock_hyf_tests() -> MutexGuard<'static, ()> {
- hyf_test_lock()
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
- }
-
- #[test]
- fn disabled_hyf_reports_disabled_state_without_spawning() {
- let _guard = lock_hyf_tests();
- let view = resolve_status(&HyfConfig {
- enabled: false,
- executable: "hyfd".into(),
- });
- assert_eq!(view.state, "disabled");
- assert_eq!(view.reason.as_deref(), Some("disabled by config"));
- }
-
- #[test]
- fn healthy_hyf_status_reports_ready() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- format!(
- "{{\"version\":{HYF_PROTOCOL_VERSION},\"request_id\":\"cli-doctor-hyf-status\",\"trace_id\":\"cli-doctor-hyf-status\",\"ok\":true,\"output\":{{\"build_identity\":{{\"protocol_version\":{HYF_PROTOCOL_VERSION}}},\"enabled_execution_modes\":{{\"deterministic\":true}}}}}}"
- )
- .as_str(),
- );
-
- let view = resolve_status(&HyfConfig {
- enabled: true,
- executable,
- });
- assert_eq!(view.state, "ready", "reason: {:?}", view.reason);
- assert_eq!(view.protocol_version, Some(HYF_PROTOCOL_VERSION));
- assert_eq!(view.deterministic_available, Some(true));
- }
-
- #[test]
- fn incompatible_hyf_status_reports_unavailable() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- "{\"version\":1,\"request_id\":\"cli-doctor-hyf-status\",\"trace_id\":\"cli-doctor-hyf-status\",\"ok\":true,\"output\":{\"build_identity\":{\"protocol_version\":2},\"enabled_execution_modes\":{\"deterministic\":true}}}",
- );
-
- let view = resolve_status(&HyfConfig {
- enabled: true,
- executable,
- });
- assert_eq!(view.state, "unavailable", "reason: {:?}", view.reason);
- assert!(
- view.reason
- .as_deref()
- .is_some_and(|reason| reason.contains("incompatible"))
- );
- }
-
- #[test]
- fn capabilities_request_uses_typed_client() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- "{\"version\":1,\"request_id\":\"cli-runtime-hyf-capabilities\",\"ok\":true,\"output\":{\"control_routes\":[\"sys.status\",\"sys.capabilities\"],\"business_capabilities\":[{\"id\":\"query_rewrite\",\"kind\":\"business\",\"deterministic_execution\":\"enabled\",\"implementation_status\":\"implemented\",\"callable\":true,\"implemented\":true,\"assisted_execution\":\"unavailable\",\"assisted_backend_available\":false}],\"assisted_backend_capabilities\":[],\"request_context_contract\":{\"accepted_features\":[\"consumer\",\"execution_mode_preference\"],\"effective_features\":[\"execution_mode_preference\"],\"unsupported_field_behavior\":\"reject\"}}}",
- );
-
- let request = request_json(
- "cli-runtime-hyf-capabilities",
- None,
- "sys.capabilities",
- None,
- &HyfEmptyInput::default(),
- );
- let response =
- HyfClient::with_timeouts(executable, Duration::from_secs(5), Duration::from_secs(5))
- .capabilities()
- .expect("capabilities");
-
- assert_eq!(request["capability"], "sys.capabilities");
- assert_eq!(request["input"], serde_json::json!({}));
- assert!(request.get("context").is_none());
- assert_eq!(
- response.output.control_routes,
- vec!["sys.status", "sys.capabilities"]
- );
- assert_eq!(response.output.business_capabilities[0].id, "query_rewrite");
- }
-
- #[test]
- fn query_rewrite_request_round_trips_typed_output() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- "{\"version\":1,\"request_id\":\"rewrite-test-1\",\"trace_id\":\"trace-rewrite-test-1\",\"ok\":true,\"output\":{\"original_text\":\"apples near me with weekend pickup\",\"normalized_text\":\"apples near me with weekend pickup\",\"rewritten_text\":\"apples\",\"query_terms\":[\"apples\"],\"normalization_signals\":[\"local_intent_detected\"],\"ranking_hints\":[\"prefer_local_results\"],\"extracted_filters\":{\"local_intent\":true,\"fulfillment\":\"pickup\",\"time_window\":\"weekend\"}},\"meta\":{\"execution_mode\":\"deterministic\",\"backend\":\"heuristic\"}}",
- );
- let context = HyfRequestContext::deterministic_cli().with_return_provenance(true);
- let request = request_json(
- "rewrite-test-1",
- Some("trace-rewrite-test-1"),
- "query_rewrite",
- Some(&context),
- &HyfQueryRewriteRequest::new("apples near me with weekend pickup"),
- );
- let client = HyfClient::new(executable);
- let response = client
- .query_rewrite(
- "rewrite-test-1",
- Some("trace-rewrite-test-1"),
- &context,
- &HyfQueryRewriteRequest::new("apples near me with weekend pickup"),
- )
- .expect("query rewrite");
- assert_eq!(request["capability"], "query_rewrite");
- assert_eq!(
- request["context"]["execution_mode_preference"],
- "deterministic"
- );
- assert_eq!(request["context"]["consumer"], "radroots-cli");
- assert_eq!(request["context"]["return_provenance"], true);
- assert_eq!(
- request["input"]["query"],
- "apples near me with weekend pickup"
- );
- assert_eq!(response.output.rewritten_text, "apples");
- assert_eq!(response.output.query_terms, vec!["apples"]);
- assert_eq!(
- response.meta,
- Some(serde_json::json!({"execution_mode":"deterministic","backend":"heuristic"}))
- );
- }
-
- #[test]
- fn business_requests_use_a_longer_timeout_than_control_requests() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_script(
- dir.path(),
- "#!/bin/sh\nread -r request || exit 64\ncase \"$request\" in\n *'\"capability\":\"sys.status\"'*)\n sleep 1\n cat <<'JSON'\n{\"version\":1,\"request_id\":\"cli-doctor-hyf-status\",\"trace_id\":\"cli-doctor-hyf-status\",\"ok\":true,\"output\":{\"build_identity\":{\"protocol_version\":1},\"enabled_execution_modes\":{\"deterministic\":true}}}\nJSON\n ;;\n *'\"capability\":\"query_rewrite\"'*)\n sleep 1\n cat <<'JSON'\n{\"version\":1,\"request_id\":\"rewrite-timeout-test\",\"trace_id\":\"rewrite-timeout-test\",\"ok\":true,\"output\":{\"original_text\":\"henhouse\",\"normalized_text\":\"henhouse\",\"rewritten_text\":\"eggs\",\"query_terms\":[\"eggs\"],\"normalization_signals\":[\"query_rewrite\"],\"ranking_hints\":[\"local_first\"],\"extracted_filters\":{\"local_intent\":false,\"fulfillment\":\"any\",\"time_window\":\"any\"}}}\nJSON\n ;;\n *)\n exit 65\n ;;\nesac\n",
- );
- let client = HyfClient::with_timeouts(
- executable,
- Duration::from_millis(100),
- Duration::from_secs(5),
- );
- assert!(client.timeouts.business > client.timeouts.control);
-
- let status = client.status().expect_err("status should time out");
- assert!(matches!(status, HyfClientError::Timeout(100)));
-
- let rewrite = client
- .query_rewrite(
- "rewrite-timeout-test",
- Some("rewrite-timeout-test"),
- &HyfRequestContext::deterministic_cli(),
- &HyfQueryRewriteRequest::new("henhouse"),
- )
- .expect("query rewrite should use longer timeout");
- assert_eq!(rewrite.output.rewritten_text, "eggs");
- }
-
- #[test]
- fn semantic_rank_request_round_trips_typed_output() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- "{\"version\":1,\"request_id\":\"rank-test-1\",\"ok\":true,\"output\":{\"ranked_ids\":[\"listing_local_1\",\"listing_regional_1\"],\"reasons\":{\"listing_local_1\":[\"apples match\",\"pickup match\"],\"listing_regional_1\":[\"delivery mismatch\"]},\"scored_candidates\":[{\"id\":\"listing_local_1\",\"heuristic_score\":14,\"matched_terms\":[\"apples\"],\"reasons\":[\"apples match\",\"pickup match\"],\"delivery_alignment\":\"match\",\"distance_band\":\"closer\",\"freshness_band\":\"fresher\",\"scope_match\":true}],\"ranking_hints\":[\"prefer_local_results\"],\"extracted_filters\":{\"local_intent\":true,\"fulfillment\":\"pickup\",\"time_window\":\"weekend\"}},\"meta\":{\"execution_mode\":\"deterministic\",\"backend\":\"heuristic\"}}",
- );
- let context = HyfRequestContext::deterministic_cli()
- .with_listing_scope(vec!["listing_local_1".to_owned()]);
- let request = request_json(
- "rank-test-1",
- None,
- "semantic_rank",
- Some(&context),
- &HyfSemanticRankRequest::new(
- "apples near me with weekend pickup",
- vec![sample_candidate("listing_local_1")],
- ),
- );
- let client = HyfClient::new(executable);
- let response = client
- .semantic_rank(
- "rank-test-1",
- None,
- &context,
- &HyfSemanticRankRequest::new(
- "apples near me with weekend pickup",
- vec![sample_candidate("listing_local_1")],
- ),
- )
- .expect("semantic rank");
-
- assert_eq!(request["capability"], "semantic_rank");
- assert_eq!(
- request["context"]["scope"]["listing_ids"],
- serde_json::json!(["listing_local_1"])
- );
- assert_eq!(request["input"]["candidates"][0]["id"], "listing_local_1");
- assert_eq!(response.output.ranked_ids[0], "listing_local_1");
- assert_eq!(response.output.scored_candidates[0].heuristic_score, 14);
- }
-
- #[test]
- fn explain_result_request_round_trips_typed_output() {
- let _guard = lock_hyf_tests();
- let dir = tempdir().expect("tempdir");
- let executable = write_response_script(
- dir.path(),
- "{\"version\":1,\"request_id\":\"explain-test-1\",\"trace_id\":\"trace-explain-test-1\",\"ok\":true,\"output\":{\"result_id\":\"listing_local_1\",\"explanation_kind\":\"deterministic\",\"summary\":\"Result listing_local_1 was ranked using deterministic heuristic signals: apples match and pickup match.\",\"score\":14,\"reasons\":[\"apples match\",\"pickup match\"],\"matched_terms\":[\"apples\"],\"ranking_hints\":[\"prefer_local_results\"],\"extracted_filters\":{\"local_intent\":true,\"fulfillment\":\"pickup\",\"time_window\":\"weekend\"},\"signal_assessment\":{\"delivery_alignment\":\"match\",\"distance_band\":\"closer\",\"freshness_band\":\"fresher\",\"scope_match\":true}},\"meta\":{\"execution_mode\":\"deterministic\",\"backend\":\"heuristic\"}}",
- );
- let context = HyfRequestContext::deterministic_cli().with_return_provenance(true);
- let request = request_json(
- "explain-test-1",
- Some("trace-explain-test-1"),
- "explain_result",
- Some(&context),
- &HyfExplainResultRequest::new(
- "apples near me with weekend pickup",
- sample_candidate("listing_local_1"),
- ),
- );
- let client = HyfClient::new(executable);
- let response = client
- .explain_result(
- "explain-test-1",
- Some("trace-explain-test-1"),
- &context,
- &HyfExplainResultRequest::new(
- "apples near me with weekend pickup",
- sample_candidate("listing_local_1"),
- ),
- )
- .expect("explain result");
-
- assert_eq!(request["capability"], "explain_result");
- assert_eq!(request["context"]["return_provenance"], true);
- assert_eq!(request["input"]["candidate"]["id"], "listing_local_1");
- assert_eq!(response.output.result_id, "listing_local_1");
- assert_eq!(
- response.output.signal_assessment.delivery_alignment,
- "match"
- );
- }
-
- fn sample_candidate(id: &str) -> HyfSemanticCandidate {
- HyfSemanticCandidate {
- id: id.to_owned(),
- title: "Organic apples".to_owned(),
- farm: "Local Orchard".to_owned(),
- delivery: "pickup".to_owned(),
- distance_km: 4.1,
- freshness_minutes: 3,
- }
- }
-
- fn request_json<T: Serialize>(
- request_id: &str,
- trace_id: Option<&str>,
- capability: &str,
- context: Option<&HyfRequestContext>,
- input: &T,
- ) -> Value {
- let raw = super::serialize_request(request_id, trace_id, capability, context, input)
- .expect("serialize request");
- serde_json::from_str(raw.as_str()).expect("request json")
- }
-
- fn write_response_script(dir: &Path, response: &str) -> PathBuf {
- write_script(
- dir,
- format!("#!/bin/sh\nread -r _request || exit 64\ncat <<'JSON'\n{response}\nJSON\n")
- .as_str(),
- )
- }
- fn write_script(dir: &Path, script: &str) -> PathBuf {
- let path = dir.join("fake-hyfd");
- fs::write(&path, script).expect("write fake hyfd");
- let mut permissions = fs::metadata(&path).expect("metadata").permissions();
- permissions.set_mode(0o755);
- fs::set_permissions(&path, permissions).expect("chmod fake hyfd");
- path
- }
-}
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -1,3067 +0,0 @@
-use std::collections::HashMap;
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_core::pricing::{Discount, DiscountScope, DiscountThreshold, DiscountValue};
-use radroots_core::{Currency, Decimal, Money, Percent, Quantity, QuantityPrice, Unit};
-use radroots_event::contract::AuthorRole;
-use radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING;
-use radroots_event::farm::FarmRef;
-use radroots_event::id::{DTag, InventoryBinId};
-use radroots_event::listing::operational::{
- OperationalListing, OperationalListingAvailability, OperationalListingBin,
- OperationalListingDeliveryMethod, OperationalListingProduct, OperationalListingPublicLocation,
- OperationalListingStatus,
-};
-use radroots_event::trade::validation::OperationalListingValidationError;
-use radroots_event_codec::d_tag::is_d_tag_base64url;
-use radroots_event_codec::operational_listing::encode::to_wire_parts_with_kind;
-use radroots_identity::PublicKey;
-use radroots_replica_store::ReplicaSql;
-use radroots_runtime_store::{RuntimeStoreRecord, RuntimeStoreRecordFamily, SourceRuntime};
-use radroots_sdk::listing::{self as sdk_listing, Plan as ListingPlan};
-use radroots_signing::{Actor, actor::ActorSource};
-use radroots_sql_core::SqlxSqliteExecutor;
-use radroots_trade::operational_listing::{
- RadrootsOperationalListingEditDocumentV1, validate_operational_listing_model,
-};
-use serde::{Deserialize, Serialize};
-use serde_json::{Value, json};
-
-use crate::cli::global::{
- ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
- ListingRebindArgs, RecordLookupArgs,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::account;
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::farm_config;
-use crate::runtime::runtime_store::{
- append_local_work, get_shared_record, list_shared_records_before, list_shared_records_latest,
- shared_runtime_store_db_path,
-};
-use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor};
-use crate::runtime::sync::{
- RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness,
-};
-use crate::view::runtime::{
- FindPriceView, FindQuantityView, FindResultProvenanceView, ListingAppRecordExportView,
- ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView,
- ListingMutationEventView, ListingMutationView, ListingNewView, ListingRebindView,
- ListingSummaryView, ListingValidateView, ListingValidationIssueView, MarketReadinessView,
-};
-
-const DRAFT_KIND: &str = "listing_draft_v1";
-const LISTING_SOURCE: &str = "local draft · local first";
-const LISTING_READ_SOURCE: &str = "local replica · local first";
-const LISTING_APP_RECORD_SOURCE: &str = "shared runtime store · app";
-const SDK_LISTING_WRITE_SOURCE: &str = "SDK listing publish · configured signer";
-const LISTING_DRAFTS_DIR: &str = "listings/drafts";
-const LISTING_SELLER_ACTOR_SOURCE_FARM_CONFIG: &str = "farm_config";
-const LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
-const LISTING_SELLER_ACTOR_SOURCE_REBIND: &str = "listing_rebind";
-const CANONICAL_OWNER_PUBKEY_REQUIRED_REASON: &str = "canonical hex pubkey required before export";
-const APP_RECORD_LIST_LIMIT: u32 = 500;
-
-static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0);
-
-fn protocol_d_tag(value: &str, field: &str) -> Result<DTag, RuntimeError> {
- value
- .parse()
- .map_err(|error| RuntimeError::Config(format!("{field} is not a valid d tag: {error}")))
-}
-
-fn protocol_inventory_bin_id(value: &str, field: &str) -> Result<InventoryBinId, RuntimeError> {
- value.parse().map_err(|error| {
- RuntimeError::Config(format!("{field} is not a valid inventory bin id: {error}"))
- })
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftDocument {
- version: u32,
- kind: String,
- listing: ListingDraftMeta,
- seller_actor: ListingDraftSellerActor,
- product: ListingDraftProduct,
- primary_bin: ListingDraftPrimaryBin,
- inventory: ListingDraftInventory,
- availability: ListingDraftAvailability,
- delivery: ListingDraftDelivery,
- location: ListingDraftLocation,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- discounts: Vec<ListingDraftDiscount>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftMeta {
- d_tag: String,
- farm_d_tag: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftSellerActor {
- account_id: String,
- pubkey: String,
- source: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftProduct {
- key: String,
- title: String,
- category: String,
- summary: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftPrimaryBin {
- bin_id: String,
- quantity_amount: String,
- quantity_unit: String,
- price_amount: String,
- price_currency: String,
- price_per_amount: String,
- price_per_unit: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- label: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftInventory {
- available: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftAvailability {
- #[serde(default, skip_serializing_if = "String::is_empty")]
- kind: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- status: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- start: Option<u64>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- end: Option<u64>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftDelivery {
- method: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftLocation {
- primary: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- city: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- region: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- country: Option<String>,
- geohash: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct ListingDraftDiscount {
- id: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- label: String,
- kind: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- value: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- amount: String,
- #[serde(default, skip_serializing_if = "String::is_empty")]
- currency: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- bin_id: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- min_bin_count: Option<u32>,
-}
-
-#[derive(Debug, Clone)]
-struct ListingValidationContext {
- farm_setup_action: String,
-}
-
-#[derive(Debug, Clone)]
-struct ListingAuthoringDefaults {
- farm_config_present: bool,
- farm_defaults_ready: bool,
- farm_next_action: Option<String>,
- farm_reason: Option<String>,
- farm_name: Option<String>,
- seller_account_id: String,
- seller_pubkey: String,
- seller_actor_source: String,
- selected_farm_d_tag: Option<String>,
- delivery_method: Option<String>,
- location: Option<ListingDraftLocation>,
-}
-
-#[derive(Debug, Clone)]
-struct CanonicalListingDraft {
- listing_id: String,
- seller_account_id: String,
- seller_pubkey: String,
- seller_actor_source: String,
- farm_d_tag: String,
- listing: OperationalListing,
-}
-
-#[derive(Debug, Clone)]
-struct SdkListingPublishInput {
- canonical: CanonicalListingDraft,
- actor: Actor,
- document: RadrootsOperationalListingEditDocumentV1,
-}
-
-#[derive(Debug, Clone)]
-struct LoadedListingDraft {
- file: PathBuf,
- updated_at_unix: u64,
- contents: String,
- document: ListingDraftDocument,
-}
-
-#[derive(Debug, Clone)]
-enum ListingDraftValidationError {
- Issue(ListingValidationIssueView),
- MissingSellerAccount(ListingValidationIssueView),
-}
-
-impl ListingDraftValidationError {
- fn into_issue(self) -> ListingValidationIssueView {
- match self {
- Self::Issue(issue) | Self::MissingSellerAccount(issue) => issue,
- }
- }
-}
-
-impl From<ListingValidationIssueView> for ListingDraftValidationError {
- fn from(issue: ListingValidationIssueView) -> Self {
- Self::Issue(issue)
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum ListingMutationOperation {
- Publish,
- Update,
- Pause,
- Withdraw,
-}
-
-impl ListingMutationOperation {
- fn as_str(self) -> &'static str {
- match self {
- Self::Publish => "publish",
- Self::Update => "update",
- Self::Pause => "pause",
- Self::Withdraw => "withdraw",
- }
- }
-
- fn listing_status(self) -> Option<&'static str> {
- match self {
- Self::Pause => Some("paused"),
- Self::Withdraw => Some("withdrawn"),
- Self::Publish | Self::Update => None,
- }
- }
-}
-
-pub fn scaffold(
- config: &RuntimeConfig,
- args: &ListingCreateArgs,
-) -> Result<ListingNewView, RuntimeError> {
- let (draft, defaults) = build_listing_draft(config, args)?;
- let output_path = listing_output_path(config, args.output.as_ref(), &draft.listing.d_tag)?;
- write_listing_draft(&output_path, &draft, false)?;
- append_listing_local_work(config, output_path.as_path(), &draft)?;
-
- let mut actions = vec![format!(
- "radroots listing publish {}",
- output_path.display()
- )];
- if let Some(action) = &defaults.farm_next_action {
- actions.push(action.clone());
- }
-
- Ok(ListingNewView {
- state: "draft created".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: output_path.display().to_string(),
- listing_id: draft.listing.d_tag,
- key: non_empty(draft.product.key.clone()),
- seller_account_id: Some(defaults.seller_account_id),
- seller_pubkey: Some(defaults.seller_pubkey),
- seller_actor_source: Some(defaults.seller_actor_source),
- farm_d_tag: defaults.selected_farm_d_tag,
- delivery_method: non_empty(draft.delivery.method.clone()),
- location_primary: non_empty(draft.location.primary.clone()),
- reason: defaults.farm_reason,
- actions,
- })
-}
-
-pub fn scaffold_preflight(
- config: &RuntimeConfig,
- args: &ListingCreateArgs,
-) -> Result<ListingNewView, RuntimeError> {
- let (draft, defaults) = build_listing_draft(config, args)?;
- let output_path = listing_output_path(config, args.output.as_ref(), &draft.listing.d_tag)?;
- validate_listing_output_target(&output_path)?;
-
- let mut actions = vec![format!(
- "radroots listing publish {}",
- output_path.display()
- )];
- if let Some(action) = &defaults.farm_next_action {
- actions.push(action.clone());
- }
-
- Ok(ListingNewView {
- state: "dry_run".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: output_path.display().to_string(),
- listing_id: draft.listing.d_tag,
- key: non_empty(draft.product.key.clone()),
- seller_account_id: Some(defaults.seller_account_id),
- seller_pubkey: Some(defaults.seller_pubkey),
- seller_actor_source: Some(defaults.seller_actor_source),
- farm_d_tag: defaults.selected_farm_d_tag,
- delivery_method: non_empty(draft.delivery.method.clone()),
- location_primary: non_empty(draft.location.primary.clone()),
- reason: Some("dry run requested; listing draft was not written".to_owned()),
- actions,
- })
-}
-
-fn build_listing_draft(
- config: &RuntimeConfig,
- args: &ListingCreateArgs,
-) -> Result<(ListingDraftDocument, ListingAuthoringDefaults), RuntimeError> {
- let defaults = authoring_defaults(config)?;
- let quantity_unit = args.quantity_unit.clone().unwrap_or_else(|| "g".to_owned());
- let draft = ListingDraftDocument {
- version: 1,
- kind: DRAFT_KIND.to_owned(),
- listing: ListingDraftMeta {
- d_tag: generate_d_tag(),
- farm_d_tag: defaults.selected_farm_d_tag.clone().unwrap_or_default(),
- },
- seller_actor: ListingDraftSellerActor {
- account_id: defaults.seller_account_id.clone(),
- pubkey: defaults.seller_pubkey.clone(),
- source: defaults.seller_actor_source.clone(),
- },
- product: ListingDraftProduct {
- key: args.key.clone().unwrap_or_default(),
- title: args.title.clone().unwrap_or_default(),
- category: args.category.clone().unwrap_or_default(),
- summary: args.summary.clone().unwrap_or_default(),
- },
- primary_bin: ListingDraftPrimaryBin {
- bin_id: args.bin_id.clone().unwrap_or_else(|| "bin-1".to_owned()),
- quantity_amount: args
- .quantity_amount
- .clone()
- .unwrap_or_else(|| "1000".to_owned()),
- quantity_unit: quantity_unit.clone(),
- price_amount: args
- .price_amount
- .clone()
- .unwrap_or_else(|| "0.01".to_owned()),
- price_currency: args
- .price_currency
- .clone()
- .unwrap_or_else(|| "USD".to_owned()),
- price_per_amount: args
- .price_per_amount
- .clone()
- .unwrap_or_else(|| "1".to_owned()),
- price_per_unit: args
- .price_per_unit
- .clone()
- .unwrap_or_else(|| quantity_unit.clone()),
- label: args.label.clone().unwrap_or_default(),
- },
- inventory: ListingDraftInventory {
- available: args.available.clone().unwrap_or_else(|| "1".to_owned()),
- },
- availability: ListingDraftAvailability {
- kind: "status".to_owned(),
- status: "active".to_owned(),
- start: None,
- end: None,
- },
- delivery: ListingDraftDelivery {
- method: defaults.delivery_method.clone().unwrap_or_default(),
- },
- location: defaults.location.clone().unwrap_or(ListingDraftLocation {
- primary: String::new(),
- city: None,
- region: None,
- country: None,
- geohash: String::new(),
- }),
- discounts: listing_discount_drafts_from_args(args),
- };
- Ok((draft, defaults))
-}
-
-fn listing_discount_drafts_from_args(args: &ListingCreateArgs) -> Vec<ListingDraftDiscount> {
- let has_discount = args.discount_id.is_some()
- || args.discount_label.is_some()
- || args.discount_kind.is_some()
- || args.discount_value.is_some()
- || args.discount_amount.is_some()
- || args.discount_currency.is_some();
- if !has_discount {
- return Vec::new();
- }
- let kind = args.discount_kind.clone().unwrap_or_else(|| {
- if args.discount_amount.is_some() {
- "amount".to_owned()
- } else {
- "percent".to_owned()
- }
- });
- vec![ListingDraftDiscount {
- id: args
- .discount_id
- .clone()
- .unwrap_or_else(|| "discount_1".to_owned()),
- label: args.discount_label.clone().unwrap_or_default(),
- kind,
- value: args.discount_value.clone().unwrap_or_default(),
- amount: args.discount_amount.clone().unwrap_or_default(),
- currency: args.discount_currency.clone().unwrap_or_default(),
- bin_id: None,
- min_bin_count: None,
- }]
-}
-
-fn listing_output_path(
- config: &RuntimeConfig,
- explicit: Option<&std::path::PathBuf>,
- listing_id: &str,
-) -> Result<std::path::PathBuf, RuntimeError> {
- match explicit {
- Some(path) => Ok(path.clone()),
- None => Ok(drafts_dir(config).join(format!("{listing_id}.toml"))),
- }
-}
-
-fn write_listing_draft(
- output_path: &Path,
- draft: &ListingDraftDocument,
- overwrite: bool,
-) -> Result<(), RuntimeError> {
- if !overwrite {
- validate_listing_output_target(output_path)?;
- }
- if let Some(parent) = output_path.parent() {
- fs::create_dir_all(parent)?;
- }
- fs::write(output_path, scaffold_contents(draft)?)?;
- Ok(())
-}
-
-fn append_listing_local_work(
- config: &RuntimeConfig,
- path: &Path,
- draft: &ListingDraftDocument,
-) -> Result<(), RuntimeError> {
- let listing_id = draft.listing.d_tag.trim();
- let seller_pubkey = draft.seller_actor.pubkey.trim();
- let listing_addr = if seller_pubkey.is_empty() || listing_id.is_empty() {
- None
- } else {
- Some(listing_addr(seller_pubkey, listing_id))
- };
- let payload = json!({
- "record_kind": DRAFT_KIND,
- "path": path.display().to_string(),
- "document": draft,
- });
- let subject = format!("listing:{}", draft.listing.d_tag);
- append_local_work(
- config,
- subject.as_str(),
- non_empty(draft.seller_actor.account_id.clone()),
- non_empty(draft.seller_actor.pubkey.clone()),
- non_empty(draft.listing.farm_d_tag.clone()),
- listing_addr,
- payload,
- )?;
- Ok(())
-}
-
-fn validate_listing_output_target(output_path: &Path) -> Result<(), RuntimeError> {
- if output_path.exists() {
- return Err(RuntimeError::Config(format!(
- "listing draft output {} must not already exist",
- output_path.display()
- )));
- }
- if let Some(parent) = output_path.parent()
- && parent.exists()
- && !parent.is_dir()
- {
- return Err(RuntimeError::Config(format!(
- "listing draft parent {} is not a directory",
- parent.display()
- )));
- }
- Ok(())
-}
-
-pub fn validate(
- config: &RuntimeConfig,
- args: &ListingFileArgs,
-) -> Result<ListingValidateView, RuntimeError> {
- let contents = fs::read_to_string(&args.file)?;
- let context = validation_context(config)?;
-
- let parsed = match toml::from_str::<ListingDraftDocument>(&contents) {
- Ok(parsed) => parsed,
- Err(error) => {
- return Ok(ListingValidateView {
- state: "invalid".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- valid: false,
- listing_id: None,
- seller_account_id: None,
- seller_pubkey: None,
- seller_actor_source: None,
- farm_d_tag: None,
- issues: vec![ListingValidationIssueView {
- field: "toml".to_owned(),
- message: error.to_string(),
- line: error
- .span()
- .map(|span| line_for_offset(&contents, span.start + 1)),
- }],
- actions: vec![format!("edit {}", args.file.display())],
- });
- }
- };
-
- match canonicalize_draft(&parsed, &contents, &context) {
- Ok(canonical) => {
- match to_wire_parts_with_kind(&canonical.listing, KIND_CLASSIFIED_LISTING) {
- Ok(_) => {}
- Err(error) => {
- return Ok(invalid_validation_view(
- args.file.as_path(),
- &parsed,
- &context,
- ListingValidationIssueView {
- field: "listing".to_owned(),
- message: format!("invalid listing contract: {error}"),
- line: None,
- },
- ));
- }
- }
- if let Some(issue) = listing_bound_account_issue(config, &canonical, &contents)? {
- return Ok(invalid_validation_view(
- args.file.as_path(),
- &parsed,
- &context,
- issue,
- ));
- }
- match validate_operational_listing_draft(&canonical) {
- Ok(_) => Ok(ListingValidateView {
- state: "valid".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- valid: true,
- listing_id: Some(canonical.listing_id),
- seller_account_id: Some(canonical.seller_account_id),
- seller_pubkey: Some(canonical.seller_pubkey),
- seller_actor_source: Some(canonical.seller_actor_source),
- farm_d_tag: Some(canonical.farm_d_tag),
- issues: Vec::new(),
- actions: vec![format!("radroots listing publish {}", args.file.display())],
- }),
- Err(error) => Ok(invalid_validation_view(
- args.file.as_path(),
- &parsed,
- &context,
- issue_from_trade_validation(error, &contents),
- )),
- }
- }
- Err(error) => Ok(invalid_validation_view(
- args.file.as_path(),
- &parsed,
- &context,
- error.into_issue(),
- )),
- }
-}
-
-pub fn list(config: &RuntimeConfig) -> Result<ListingListView, RuntimeError> {
- let dir = drafts_dir(config);
- if !dir.exists() {
- return Ok(ListingListView {
- state: "empty".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- count: 0,
- draft_dir: dir.display().to_string(),
- listings: Vec::new(),
- actions: vec!["radroots listing create".to_owned()],
- });
- }
-
- let context = validation_context(config).map_err(|error| error.to_string());
- let mut listings = Vec::new();
- for entry in fs::read_dir(&dir)? {
- let entry = entry?;
- let path = entry.path();
- if path.extension().and_then(|value| value.to_str()) != Some("toml") {
- continue;
- }
- match load_listing_draft(path.as_path()) {
- Ok(loaded) => listings.push(summary_from_loaded(config, &loaded, context.as_ref())),
- Err(issue) => listings.push(summary_for_invalid_file(path.as_path(), issue)),
- }
- }
-
- listings.sort_by(|left, right| {
- right
- .updated_at_unix
- .cmp(&left.updated_at_unix)
- .then_with(|| left.id.cmp(&right.id))
- });
-
- let state = if listings.is_empty() {
- "empty"
- } else if listings.iter().any(|listing| listing.state == "error") {
- "degraded"
- } else {
- "ready"
- };
- let actions = if listings.is_empty() {
- vec!["radroots listing create".to_owned()]
- } else {
- Vec::new()
- };
-
- Ok(ListingListView {
- state: state.to_owned(),
- source: LISTING_SOURCE.to_owned(),
- count: listings.len(),
- draft_dir: dir.display().to_string(),
- listings,
- actions,
- })
-}
-
-pub fn app_record_list(config: &RuntimeConfig) -> Result<ListingAppRecordListView, RuntimeError> {
- let database_path = shared_runtime_store_db_path(config)?;
- let mut entries = current_app_record_entries(app_local_records(config)?);
- let has_more = entries.len() > APP_RECORD_LIST_LIMIT as usize;
- if has_more {
- entries.truncate(APP_RECORD_LIST_LIMIT as usize);
- }
- let next_cursor = if has_more {
- entries
- .last()
- .map(|entry| (entry.record.change_seq, entry.record.seq))
- } else {
- None
- };
- let records = entries
- .iter()
- .map(|entry| app_record_summary(&entry.record, entry.superseded_count))
- .collect::<Vec<_>>();
- let state = if records.is_empty() { "empty" } else { "ready" };
- let actions = if records.is_empty() {
- vec!["create or save a farm listing in radroots_studio_app".to_owned()]
- } else {
- Vec::new()
- };
-
- Ok(ListingAppRecordListView {
- state: state.to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- count: records.len(),
- limit: APP_RECORD_LIST_LIMIT,
- has_more,
- next_before_change_seq: next_cursor.map(|(change_seq, _)| change_seq),
- next_before_seq: next_cursor.map(|(_, seq)| seq),
- runtime_store_db: database_path.display().to_string(),
- records,
- actions,
- })
-}
-
-pub fn app_record_export(
- config: &RuntimeConfig,
- args: &ListingAppRecordExportArgs,
-) -> Result<ListingAppRecordExportView, RuntimeError> {
- let Some(record) = get_shared_record(config, args.record_id.as_str())? else {
- return Ok(ListingAppRecordExportView {
- state: "missing".to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: args
- .output
- .as_ref()
- .map(|path| path.display().to_string())
- .unwrap_or_default(),
- valid: false,
- listing_id: None,
- listing_addr: None,
- seller_account_id: None,
- seller_pubkey: None,
- seller_actor_source: None,
- farm_d_tag: None,
- issues: Vec::new(),
- reason: Some(format!(
- "app-authored local record `{}` was not found",
- args.record_id
- )),
- actions: vec!["radroots listing app list".to_owned()],
- });
- };
-
- if let Some(current_record) = current_app_record_for(config, &record)?
- && current_record.record_id != record.record_id
- {
- let (listing_id, title, farm_d_tag) = app_listing_display_parts(&record);
- let current_action = format!("radroots listing app export {}", current_record.record_id);
- return Ok(ListingAppRecordExportView {
- state: "stale".to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: args
- .output
- .as_ref()
- .map(|path| path.display().to_string())
- .unwrap_or_default(),
- valid: false,
- listing_id,
- listing_addr: record.listing_addr.clone(),
- seller_account_id: record.owner_account_id.clone(),
- seller_pubkey: record.owner_pubkey.clone(),
- seller_actor_source: None,
- farm_d_tag: farm_d_tag.or(record.farm_id.clone()),
- issues: vec![ListingValidationIssueView {
- field: "record_id".to_owned(),
- message: format!(
- "app-authored local record `{}` was superseded by `{}`",
- record.record_id, current_record.record_id
- ),
- line: None,
- }],
- reason: Some(format!(
- "app-authored local record `{}` was superseded by current record `{}`{}",
- record.record_id,
- current_record.record_id,
- title
- .as_deref()
- .map(|value| format!(" for `{value}`"))
- .unwrap_or_default()
- )),
- actions: vec![current_action, "radroots listing app list".to_owned()],
- });
- }
-
- let draft = match app_listing_draft_from_record(&record) {
- Ok(draft) => draft,
- Err(reason) => {
- return Ok(ListingAppRecordExportView {
- state: "unsupported".to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: args
- .output
- .as_ref()
- .map(|path| path.display().to_string())
- .unwrap_or_default(),
- valid: false,
- listing_id: None,
- listing_addr: record.listing_addr.clone(),
- seller_account_id: record.owner_account_id.clone(),
- seller_pubkey: record.owner_pubkey.clone(),
- seller_actor_source: None,
- farm_d_tag: record.farm_id.clone(),
- issues: vec![ListingValidationIssueView {
- field: "local_work_json".to_owned(),
- message: reason.clone(),
- line: None,
- }],
- reason: Some(reason),
- actions: vec!["radroots listing app list".to_owned()],
- });
- }
- };
- let output_path = listing_output_path(config, args.output.as_ref(), &draft.listing.d_tag)?;
- validate_listing_output_target(output_path.as_path())?;
- let contents = scaffold_contents(&draft)?;
- let context = validation_context(config)?;
- let issues = app_listing_export_issues(config, &draft, contents.as_str(), &context)?;
- let listing_addr_value = app_record_listing_addr(&draft);
-
- if !issues.is_empty() {
- return Ok(ListingAppRecordExportView {
- state: "invalid".to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: output_path.display().to_string(),
- valid: false,
- listing_id: non_empty(draft.listing.d_tag.clone()),
- listing_addr: listing_addr_value,
- seller_account_id: non_empty(draft.seller_actor.account_id.clone()),
- seller_pubkey: non_empty(draft.seller_actor.pubkey.clone()),
- seller_actor_source: non_empty(draft.seller_actor.source.clone()),
- farm_d_tag: non_empty(draft.listing.farm_d_tag.clone()),
- issues,
- reason: Some(format!(
- "app-authored local record `{}` does not validate as a CLI listing draft",
- args.record_id
- )),
- actions: vec!["radroots listing app list".to_owned()],
- });
- }
-
- if !config.output.dry_run {
- write_listing_draft(output_path.as_path(), &draft, false)?;
- }
-
- Ok(ListingAppRecordExportView {
- state: if config.output.dry_run {
- "dry_run"
- } else {
- "exported"
- }
- .to_owned(),
- source: LISTING_APP_RECORD_SOURCE.to_owned(),
- record_id: args.record_id.clone(),
- dry_run: config.output.dry_run,
- file: output_path.display().to_string(),
- valid: true,
- listing_id: Some(draft.listing.d_tag.clone()),
- listing_addr: app_record_listing_addr(&draft),
- seller_account_id: Some(draft.seller_actor.account_id.clone()),
- seller_pubkey: Some(draft.seller_actor.pubkey.clone()),
- seller_actor_source: Some(draft.seller_actor.source.clone()),
- farm_d_tag: Some(draft.listing.farm_d_tag.clone()),
- issues: Vec::new(),
- reason: Some(if config.output.dry_run {
- "dry run requested; listing draft was not written".to_owned()
- } else {
- "app-authored listing record exported as a CLI listing draft".to_owned()
- }),
- actions: vec![
- format!("radroots listing publish {}", output_path.display()),
- format!("radroots listing publish {}", output_path.display()),
- ],
- })
-}
-
-pub fn rebind(
- config: &RuntimeConfig,
- args: &ListingRebindArgs,
-) -> Result<ListingRebindView, RuntimeError> {
- rebind_inner(config, args, false)
-}
-
-pub fn rebind_preflight(
- config: &RuntimeConfig,
- args: &ListingRebindArgs,
-) -> Result<ListingRebindView, RuntimeError> {
- rebind_inner(config, args, true)
-}
-
-fn rebind_inner(
- config: &RuntimeConfig,
- args: &ListingRebindArgs,
- dry_run: bool,
-) -> Result<ListingRebindView, RuntimeError> {
- let contents = fs::read_to_string(&args.file)?;
- let mut draft = toml::from_str::<ListingDraftDocument>(&contents).map_err(|error| {
- RuntimeError::Config(format!(
- "invalid listing draft {}: {error}",
- args.file.display()
- ))
- })?;
- let listing_id = draft.listing.d_tag.trim().to_owned();
- if !is_d_tag_base64url(&listing_id) {
- return Err(RuntimeError::Config(format!(
- "invalid listing draft {}: listing d_tag must be a 22-character base64url identifier",
- args.file.display()
- )));
- }
-
- let target_account = account::resolve_account_selector(config, args.selector.as_str())
- .map_err(|error| listing_rebind_selector_error(args.selector.as_str(), error))?;
- let from_seller_account_id = non_empty(draft.seller_actor.account_id.clone());
- let from_seller_pubkey = non_empty(draft.seller_actor.pubkey.clone());
- let from_seller_actor_source = non_empty(draft.seller_actor.source.clone());
- let from_farm_d_tag = non_empty(draft.listing.farm_d_tag.clone());
- let target_account_id = target_account.record.id().to_string();
- let target_pubkey = target_account
- .record
- .public_identity()
- .public_key()
- .to_hex();
- let target_farm_d_tag = resolve_rebind_farm_d_tag(
- config,
- args,
- from_seller_account_id.as_deref(),
- from_farm_d_tag.as_deref(),
- target_account_id.as_str(),
- )?;
- let from_listing_addr = from_seller_pubkey
- .as_ref()
- .map(|pubkey| listing_addr(pubkey, listing_id.as_str()));
- let to_listing_addr = listing_addr(target_pubkey.as_str(), listing_id.as_str());
- let seller_pubkey_changed = from_seller_pubkey
- .as_deref()
- .map(|pubkey| !pubkey.eq_ignore_ascii_case(target_pubkey.as_str()));
- let listing_addr_changed = from_listing_addr
- .as_deref()
- .map(|addr| addr != to_listing_addr.as_str());
- let farm_d_tag_changed = from_farm_d_tag
- .as_deref()
- .map(|d_tag| d_tag != target_farm_d_tag.as_str());
-
- draft.seller_actor.account_id = target_account_id.clone();
- draft.seller_actor.pubkey = target_pubkey.clone();
- draft.seller_actor.source = LISTING_SELLER_ACTOR_SOURCE_REBIND.to_owned();
- draft.listing.farm_d_tag = target_farm_d_tag.clone();
-
- if !dry_run {
- write_listing_draft(args.file.as_path(), &draft, true)?;
- append_listing_local_work(config, args.file.as_path(), &draft)?;
- }
-
- Ok(ListingRebindView {
- state: if dry_run { "dry_run" } else { "rebound" }.to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id,
- dry_run,
- from_seller_account_id,
- from_seller_pubkey,
- from_seller_actor_source,
- to_seller_account_id: target_account_id,
- to_seller_pubkey: target_pubkey,
- to_seller_actor_source: LISTING_SELLER_ACTOR_SOURCE_REBIND.to_owned(),
- seller_pubkey_changed,
- from_listing_addr,
- to_listing_addr,
- listing_addr_changed,
- from_farm_d_tag,
- to_farm_d_tag: target_farm_d_tag,
- farm_d_tag_changed,
- reason: Some(if dry_run {
- "dry run requested; listing seller actor binding was not written".to_owned()
- } else {
- "listing seller actor binding updated".to_owned()
- }),
- actions: if dry_run {
- vec![format!("radroots account select {}", args.selector)]
- } else {
- vec![format!("radroots listing publish {}", args.file.display())]
- },
- })
-}
-
-fn resolve_rebind_farm_d_tag(
- config: &RuntimeConfig,
- args: &ListingRebindArgs,
- from_seller_account_id: Option<&str>,
- from_farm_d_tag: Option<&str>,
- target_account_id: &str,
-) -> Result<String, RuntimeError> {
- if let Some(explicit) = args
- .farm_d_tag
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- if !is_d_tag_base64url(explicit) {
- return Err(RuntimeError::Config(
- "listing rebind --farm-d-tag must be a 22-character base64url identifier"
- .to_owned(),
- ));
- }
- return Ok(explicit.to_owned());
- }
- if from_seller_account_id == Some(target_account_id)
- && let Some(existing) = from_farm_d_tag
- {
- return Ok(existing.to_owned());
- }
- if let Some(resolved) = farm_config::load(config, None)?
- && resolved.document.selection.account == target_account_id
- {
- return Ok(resolved.document.selection.farm_d_tag);
- }
- Err(RuntimeError::Config(format!(
- "listing rebind requires --farm-d-tag when target account `{target_account_id}` is not bound by the selected farm config"
- )))
-}
-
-fn listing_rebind_selector_error(selector: &str, error: RuntimeError) -> RuntimeError {
- match error {
- RuntimeError::Account(account::AccountRuntimeFailure::Unresolved(issue)) => {
- account::AccountRuntimeFailure::unresolved_with_detail(
- issue.message().to_owned(),
- json!({
- "seller_actor_source": LISTING_SELLER_ACTOR_SOURCE_REBIND,
- "selector": selector,
- "actions": [
- "radroots account import <path>",
- "radroots account create",
- ],
- }),
- )
- .into()
- }
- other => other,
- }
-}
-
-fn listing_addr(seller_pubkey: &str, listing_id: &str) -> String {
- format!("{KIND_CLASSIFIED_LISTING}:{seller_pubkey}:{listing_id}")
-}
-
-fn load_listing_draft(path: &Path) -> Result<LoadedListingDraft, ListingValidationIssueView> {
- let contents = fs::read_to_string(path).map_err(|error| ListingValidationIssueView {
- field: "file".to_owned(),
- message: format!("read listing draft {}: {error}", path.display()),
- line: None,
- })?;
- let document = toml::from_str::<ListingDraftDocument>(contents.as_str()).map_err(|error| {
- ListingValidationIssueView {
- field: "toml".to_owned(),
- message: error.to_string(),
- line: error
- .span()
- .map(|span| line_for_offset(contents.as_str(), span.start + 1)),
- }
- })?;
- Ok(LoadedListingDraft {
- file: path.to_path_buf(),
- updated_at_unix: modified_unix(path).unwrap_or_default(),
- contents,
- document,
- })
-}
-
-fn summary_from_loaded(
- config: &RuntimeConfig,
- loaded: &LoadedListingDraft,
- context: Result<&ListingValidationContext, &String>,
-) -> ListingSummaryView {
- let mut seller_account_id = non_empty(loaded.document.seller_actor.account_id.clone());
- let mut seller_pubkey = non_empty(loaded.document.seller_actor.pubkey.clone());
- let mut seller_actor_source = non_empty(loaded.document.seller_actor.source.clone());
- let mut farm_d_tag = non_empty(loaded.document.listing.farm_d_tag.clone());
- let mut issues = Vec::new();
- let mut state = "draft";
-
- match context {
- Ok(context) => {
- match canonicalize_draft(&loaded.document, loaded.contents.as_str(), context) {
- Ok(canonical) => {
- seller_account_id = Some(canonical.seller_account_id.clone());
- seller_pubkey = Some(canonical.seller_pubkey.clone());
- seller_actor_source = Some(canonical.seller_actor_source.clone());
- farm_d_tag = Some(canonical.farm_d_tag.clone());
- issues = listing_ready_issues(&canonical, loaded.contents.as_str());
- if let Ok(Some(issue)) =
- listing_bound_account_issue(config, &canonical, loaded.contents.as_str())
- {
- issues.push(issue);
- }
- if issues.is_empty() {
- state = "ready";
- }
- }
- Err(error) => issues.push(error.into_issue()),
- }
- }
- Err(reason) => issues.push(ListingValidationIssueView {
- field: "context".to_owned(),
- message: reason.to_string(),
- line: None,
- }),
- }
-
- ListingSummaryView {
- id: non_empty(loaded.document.listing.d_tag.clone())
- .unwrap_or_else(|| file_stem(loaded.file.as_path())),
- state: state.to_owned(),
- file: loaded.file.display().to_string(),
- product_key: non_empty(loaded.document.product.key.clone()),
- title: non_empty(loaded.document.product.title.clone()),
- category: non_empty(loaded.document.product.category.clone()),
- seller_account_id,
- seller_pubkey,
- seller_actor_source,
- farm_d_tag,
- location_primary: non_empty(loaded.document.location.primary.clone()),
- updated_at_unix: loaded.updated_at_unix,
- issues,
- }
-}
-
-fn listing_ready_issues(
- canonical: &CanonicalListingDraft,
- contents: &str,
-) -> Vec<ListingValidationIssueView> {
- match to_wire_parts_with_kind(&canonical.listing, KIND_CLASSIFIED_LISTING) {
- Ok(_) => {}
- Err(error) => {
- return vec![ListingValidationIssueView {
- field: "listing".to_owned(),
- message: format!("invalid listing contract: {error}"),
- line: None,
- }];
- }
- }
- match validate_operational_listing_draft(canonical) {
- Ok(_) => Vec::new(),
- Err(error) => vec![issue_from_trade_validation(error, contents)],
- }
-}
-
-fn summary_for_invalid_file(path: &Path, issue: ListingValidationIssueView) -> ListingSummaryView {
- ListingSummaryView {
- id: file_stem(path),
- state: "error".to_owned(),
- file: path.display().to_string(),
- product_key: None,
- title: None,
- category: None,
- seller_account_id: None,
- seller_pubkey: None,
- seller_actor_source: None,
- farm_d_tag: None,
- location_primary: None,
- updated_at_unix: modified_unix(path).unwrap_or_default(),
- issues: vec![issue],
- }
-}
-
-#[derive(Debug, Clone)]
-struct AppRecordListEntry {
- record: RuntimeStoreRecord,
- superseded_count: usize,
-}
-
-fn app_local_records(config: &RuntimeConfig) -> Result<Vec<RuntimeStoreRecord>, RuntimeError> {
- let mut app_records = Vec::new();
- let mut before_cursor = None::<(i64, i64)>;
- loop {
- let shared_records = if let Some((before_change_seq, before_seq)) = before_cursor {
- list_shared_records_before(
- config,
- before_change_seq,
- before_seq,
- APP_RECORD_LIST_LIMIT,
- )?
- } else {
- list_shared_records_latest(config, APP_RECORD_LIST_LIMIT)?
- };
- let Some(next_cursor) = shared_records
- .last()
- .map(|record| (record.change_seq, record.seq))
- else {
- break;
- };
- let has_more = shared_records.len() == APP_RECORD_LIST_LIMIT as usize;
- app_records.extend(
- shared_records
- .into_iter()
- .filter(is_supported_app_local_record),
- );
- if !has_more {
- break;
- }
- before_cursor = Some(next_cursor);
- }
- Ok(app_records)
-}
-
-fn is_supported_app_local_record(record: &RuntimeStoreRecord) -> bool {
- record.source_runtime == SourceRuntime::App
- && record.family == RuntimeStoreRecordFamily::LocalWork
- && matches!(
- local_record_kind(record).as_deref(),
- Some("farm_config_v1" | DRAFT_KIND)
- )
-}
-
-fn current_app_record_entries(mut records: Vec<RuntimeStoreRecord>) -> Vec<AppRecordListEntry> {
- records.sort_by(|left, right| {
- right
- .change_seq
- .cmp(&left.change_seq)
- .then_with(|| right.seq.cmp(&left.seq))
- .then_with(|| left.record_id.cmp(&right.record_id))
- });
-
- let mut entries: Vec<AppRecordListEntry> = Vec::new();
- let mut seen = HashMap::<String, usize>::new();
- for record in records {
- let key = app_record_current_key(&record);
- if let Some(index) = seen.get(&key).copied() {
- entries[index].superseded_count += 1;
- } else {
- seen.insert(key, entries.len());
- entries.push(AppRecordListEntry {
- record,
- superseded_count: 0,
- });
- }
- }
- entries
-}
-
-fn current_app_record_for(
- config: &RuntimeConfig,
- record: &RuntimeStoreRecord,
-) -> Result<Option<RuntimeStoreRecord>, RuntimeError> {
- let key = app_record_current_key(record);
- Ok(app_local_records(config)?
- .into_iter()
- .filter(|candidate| app_record_current_key(candidate) == key)
- .max_by(|left, right| {
- left.change_seq
- .cmp(&right.change_seq)
- .then_with(|| left.seq.cmp(&right.seq))
- }))
-}
-
-fn app_record_summary(
- record: &RuntimeStoreRecord,
- superseded_count: usize,
-) -> ListingAppRecordSummaryView {
- let record_kind = local_record_kind(record).unwrap_or_else(|| "unknown".to_owned());
- let (listing_id, title, exportable, reason) = match record_kind.as_str() {
- DRAFT_KIND => {
- if let Some(reason) = app_record_exportability_reason(record) {
- let (listing_id, title, _) = app_listing_display_parts(record);
- (listing_id, title, false, Some(reason))
- } else {
- match app_listing_draft_from_record(record) {
- Ok(draft) => (
- non_empty(draft.listing.d_tag),
- non_empty(draft.product.title),
- true,
- None,
- ),
- Err(reason) => {
- let (listing_id, title, _) = app_listing_display_parts(record);
- (listing_id, title, false, Some(reason))
- }
- }
- }
- }
- "farm_config_v1" => (
- None,
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["document"]["farm"]["name"].as_str())
- .map(str::to_owned),
- false,
- Some("farm records provide defaults; export selects listing records".to_owned()),
- ),
- _ => (
- None,
- None,
- false,
- Some(format!("unsupported app record kind `{record_kind}`")),
- ),
- };
- let actions = if exportable {
- vec![format!("radroots listing app export {}", record.record_id)]
- } else {
- Vec::new()
- };
-
- ListingAppRecordSummaryView {
- record_id: record.record_id.clone(),
- seq: record.seq,
- change_seq: record.change_seq,
- superseded_count,
- record_kind,
- status: record.status.as_str().to_owned(),
- source_runtime: record.source_runtime.as_str().to_owned(),
- owner_account_id: record.owner_account_id.clone(),
- owner_pubkey: record.owner_pubkey.clone(),
- farm_id: record.farm_id.clone(),
- listing_addr: record.listing_addr.clone(),
- listing_id,
- title,
- exportable,
- reason,
- actions,
- }
-}
-
-fn app_record_current_key(record: &RuntimeStoreRecord) -> String {
- match local_record_kind(record).as_deref() {
- Some(DRAFT_KIND) => {
- if let Some(listing_addr) = record
- .listing_addr
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- return format!("listing_addr:{listing_addr}");
- }
- let (listing_id, _, _) = app_listing_display_parts(record);
- if let (Some(owner_pubkey), Some(listing_id)) = (
- app_record_canonical_owner_pubkey(record),
- listing_id.filter(|value| is_d_tag_base64url(value)),
- ) {
- return format!("listing_owner:{owner_pubkey}:{listing_id}");
- }
- }
- Some("farm_config_v1") => {
- if let Some(farm_id) = record
- .farm_id
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- return format!("farm:{farm_id}");
- }
- if let Some(farm_id) = record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload["document"]["farm"]["d_tag"].as_str())
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- return format!("farm:{farm_id}");
- }
- }
- _ => {}
- }
- format!("record:{}", record.record_id)
-}
-
-fn canonical_hex_pubkey(value: &str) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.len() == 64 && trimmed.chars().all(|char| char.is_ascii_hexdigit()) {
- Some(trimmed.to_ascii_lowercase())
- } else {
- None
- }
-}
-
-fn app_record_canonical_owner_pubkey(record: &RuntimeStoreRecord) -> Option<String> {
- record
- .owner_pubkey
- .as_deref()
- .and_then(canonical_hex_pubkey)
-}
-
-fn app_listing_display_parts(
- record: &RuntimeStoreRecord,
-) -> (Option<String>, Option<String>, Option<String>) {
- let document = record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload.get("document"));
- let listing_id = document
- .and_then(|document| document["listing"]["d_tag"].as_str())
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned);
- let title = document
- .and_then(|document| document["product"]["title"].as_str())
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned);
- let farm_d_tag = document
- .and_then(|document| document["listing"]["farm_d_tag"].as_str())
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned);
- (listing_id, title, farm_d_tag)
-}
-
-fn app_record_exportability_reason(record: &RuntimeStoreRecord) -> Option<String> {
- if local_record_kind(record).as_deref() == Some(DRAFT_KIND)
- && app_record_canonical_owner_pubkey(record).is_none()
- {
- return Some(CANONICAL_OWNER_PUBKEY_REQUIRED_REASON.to_owned());
- }
- let exportability = record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload.get("exportability"))?;
- let state = exportability
- .get("state")
- .and_then(Value::as_str)
- .unwrap_or_default();
- if state.is_empty() || state == "exportable" {
- return None;
- }
- let reason = exportability
- .get("reason")
- .and_then(Value::as_str)
- .unwrap_or_default();
- Some(match (state, reason) {
- ("identity_unresolved", "canonical_hex_pubkey_required") => {
- CANONICAL_OWNER_PUBKEY_REQUIRED_REASON.to_owned()
- }
- ("identity_unresolved", _) => "app record identity is unresolved".to_owned(),
- (_, "") => format!("app record exportability state `{state}` is not exportable"),
- (_, reason) => format!("app record exportability state `{state}`: {reason}"),
- })
-}
-
-fn app_listing_draft_from_record(
- record: &RuntimeStoreRecord,
-) -> Result<ListingDraftDocument, String> {
- if record.source_runtime != SourceRuntime::App {
- return Err(format!(
- "record source_runtime `{}` is not app",
- record.source_runtime.as_str()
- ));
- }
- if record.family != RuntimeStoreRecordFamily::LocalWork {
- return Err(format!(
- "record family `{}` is not local_work",
- record.family.as_str()
- ));
- }
- let payload = record
- .local_work_json
- .as_ref()
- .ok_or_else(|| "record has no local_work_json payload".to_owned())?;
- let record_kind = local_record_kind(record).unwrap_or_else(|| "unknown".to_owned());
- if record_kind != DRAFT_KIND {
- return Err(format!("record kind `{record_kind}` is not {DRAFT_KIND}"));
- }
- if let Some(reason) = app_record_exportability_reason(record) {
- return Err(reason);
- }
- let owner_pubkey = app_record_canonical_owner_pubkey(record)
- .ok_or_else(|| CANONICAL_OWNER_PUBKEY_REQUIRED_REASON.to_owned())?;
- let document = payload
- .get("document")
- .cloned()
- .ok_or_else(|| "record local_work_json.document is missing".to_owned())?;
- let mut draft = serde_json::from_value::<ListingDraftDocument>(document)
- .map_err(|error| format!("record listing document is invalid: {error}"))?;
- if let Some(account_id) = record
- .owner_account_id
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- draft.seller_actor.account_id = account_id.to_owned();
- }
- draft.seller_actor.pubkey = owner_pubkey;
- if draft.listing.farm_d_tag.trim().is_empty()
- && let Some(farm_id) = record.farm_id.as_ref()
- {
- draft.listing.farm_d_tag = farm_id.clone();
- }
- normalize_app_listing_availability(&mut draft)?;
- normalize_app_listing_units(&mut draft);
- Ok(draft)
-}
-
-fn normalize_app_listing_availability(draft: &mut ListingDraftDocument) -> Result<(), String> {
- let kind = draft.availability.kind.trim();
- if kind.is_empty() || kind == "local" {
- draft.availability.kind = "status".to_owned();
- } else if !matches!(kind, "status" | "window") {
- return Err(format!(
- "unsupported app listing availability kind `{kind}`"
- ));
- }
- if draft.availability.kind == "window" {
- return Ok(());
- }
-
- let status = draft.availability.status.trim();
- draft.availability.status = match status {
- "" | "active" | "draft" | "published" => "active".to_owned(),
- "archived" | "paused" | "sold" => {
- return Err(format!(
- "app listing status `{status}` is not exportable as a publishable CLI draft"
- ));
- }
- other => return Err(format!("unsupported app listing status `{other}`")),
- };
- Ok(())
-}
-
-fn normalize_app_listing_units(draft: &mut ListingDraftDocument) {
- let quantity_unit = draft.primary_bin.quantity_unit.trim().to_owned();
- let price_per_unit = draft.primary_bin.price_per_unit.trim().to_owned();
- let quantity_unit_supported = quantity_unit.parse::<Unit>().is_ok();
- let price_per_unit_supported = price_per_unit.parse::<Unit>().is_ok();
- if quantity_unit_supported && price_per_unit_supported {
- return;
- }
-
- if draft.primary_bin.label.trim().is_empty() {
- draft.primary_bin.label = if !quantity_unit_supported && !quantity_unit.is_empty() {
- quantity_unit.clone()
- } else {
- price_per_unit.clone()
- };
- }
- if !quantity_unit_supported {
- draft.primary_bin.quantity_unit = "each".to_owned();
- }
- if !price_per_unit_supported {
- draft.primary_bin.price_per_unit = "each".to_owned();
- }
-}
-
-fn app_listing_export_issues(
- config: &RuntimeConfig,
- draft: &ListingDraftDocument,
- contents: &str,
- context: &ListingValidationContext,
-) -> Result<Vec<ListingValidationIssueView>, RuntimeError> {
- let canonical = match canonicalize_draft(draft, contents, context) {
- Ok(canonical) => canonical,
- Err(error) => return Ok(vec![error.into_issue()]),
- };
- let mut issues = listing_ready_issues(&canonical, contents);
- if let Some(issue) = listing_bound_account_issue(config, &canonical, contents)? {
- issues.push(issue);
- }
- Ok(issues)
-}
-
-fn app_record_listing_addr(draft: &ListingDraftDocument) -> Option<String> {
- let seller_pubkey = draft.seller_actor.pubkey.trim();
- let listing_id = draft.listing.d_tag.trim();
- if seller_pubkey.is_empty() || listing_id.is_empty() {
- None
- } else {
- Some(listing_addr(seller_pubkey, listing_id))
- }
-}
-
-fn local_record_kind(record: &RuntimeStoreRecord) -> Option<String> {
- record
- .local_work_json
- .as_ref()
- .and_then(|payload| payload.get("record_kind"))
- .and_then(Value::as_str)
- .map(str::to_owned)
-}
-
-pub fn get(
- config: &RuntimeConfig,
- args: &RecordLookupArgs,
-) -> Result<ListingGetView, RuntimeError> {
- refresh_market_listing_if_needed(config)?;
- let freshness = if config.local.replica_store_path.exists() {
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?
- } else {
- missing_freshness()
- };
- let provenance = FindResultProvenanceView {
- origin: "local_replica.trade_product".to_owned(),
- freshness: freshness.display.clone(),
- relay_count: config.transport.nostr_relay_urls.len(),
- };
-
- if !config.local.replica_store_path.exists() {
- return Ok(ListingGetView {
- state: "unconfigured".to_owned(),
- source: LISTING_READ_SOURCE.to_owned(),
- lookup: args.key.clone(),
- readiness: MarketReadinessView::unavailable("local_replica_not_initialized"),
- listing_id: None,
- product_key: None,
- listing_addr: None,
- primary_bin_id: None,
- title: None,
- category: None,
- description: None,
- location_primary: None,
- available: None,
- price: None,
- provenance,
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store inspect".to_owned()],
- });
- }
-
- let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?);
- let rows = db.trade_product_lookup(args.key.as_str())?;
- let Some(row) = rows.into_iter().next() else {
- return Ok(ListingGetView {
- state: "missing".to_owned(),
- source: LISTING_READ_SOURCE.to_owned(),
- lookup: args.key.clone(),
- readiness: MarketReadinessView::unavailable("market_listing_missing"),
- listing_id: None,
- product_key: None,
- listing_addr: None,
- primary_bin_id: None,
- title: None,
- category: None,
- description: None,
- location_primary: None,
- available: None,
- price: None,
- provenance,
- reason: Some(format!(
- "listing `{}` is not available in the local replica",
- args.key
- )),
- actions: vec![
- "radroots sync pull".to_owned(),
- format!("radroots market search {}", args.key),
- ],
- });
- };
-
- let listing_addr = row.listing_addr.and_then(non_empty);
- let primary_bin_id = row.primary_bin_id.and_then(non_empty);
- let verified_primary_bin_id = row.verified_primary_bin_id.and_then(non_empty);
- let available_amount = row.qty_avail;
- let price_amount = row.price_amt;
- let price_currency = row.price_currency;
- let price_per_amount = row.price_qty_amt;
- let readiness = MarketReadinessView::from_market_projection(
- listing_addr.as_deref(),
- primary_bin_id.as_deref(),
- verified_primary_bin_id.as_deref(),
- Some(row.title.as_str()),
- Some(row.category.as_str()),
- available_amount,
- price_amount,
- price_currency.as_str(),
- price_per_amount,
- );
-
- Ok(ListingGetView {
- state: "ready".to_owned(),
- source: LISTING_READ_SOURCE.to_owned(),
- lookup: args.key.clone(),
- readiness,
- listing_id: Some(row.id),
- product_key: Some(row.key),
- listing_addr,
- primary_bin_id,
- title: Some(row.title),
- category: Some(row.category),
- description: non_empty(row.summary),
- location_primary: row.location_primary.and_then(non_empty),
- available: Some(FindQuantityView {
- total_amount: row.qty_amt,
- total_unit: row.qty_unit,
- label: row.qty_label.and_then(non_empty),
- available_amount,
- }),
- price: Some(FindPriceView {
- amount: price_amount,
- currency: price_currency,
- per_amount: price_per_amount,
- per_unit: row.price_qty_unit,
- }),
- provenance,
- reason: None,
- actions: Vec::new(),
- })
-}
-
-fn refresh_market_listing_if_needed(config: &RuntimeConfig) -> Result<(), RuntimeError> {
- if !config.local.replica_store_path.exists()
- || config.output.dry_run
- || config.transport.nostr_relay_urls.is_empty()
- {
- return Ok(());
- }
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- let freshness =
- freshness_for_scope_from_executor(config, &executor, RelayIngestScope::MarketPull)?;
- if crate::runtime::sync::freshness_requires_refresh(&freshness) {
- let _ = market_refresh(config)?;
- }
- Ok(())
-}
-
-pub fn publish_via_sdk(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- let input = sdk_listing_publish_input(config, args)?;
- validate_configured_listing_signer(config, &input.canonical)?;
- let plan = sdk_listing::prepare(sdk_listing::PrepareRequest::publish(
- input.actor,
- input.document,
- sdk_created_at_unix()?,
- ))
- .map_err(|error| RuntimeError::Config(format!("invalid SDK listing plan: {error}")))?;
- if !config.output.dry_run {
- return Err(RuntimeError::Config(
- "listing commit is unavailable until the shared sync engine is configured".to_owned(),
- )
- .into());
- }
- Ok(sdk_prepared_publish_view(
- config,
- args,
- ListingMutationOperation::Publish,
- &input.canonical,
- plan,
- ))
-}
-
-fn sdk_listing_publish_input(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
-) -> Result<SdkListingPublishInput, RuntimeError> {
- let contents = fs::read_to_string(&args.file)?;
- let parsed = toml::from_str::<ListingDraftDocument>(&contents).map_err(|error| {
- RuntimeError::Config(format!(
- "invalid listing draft {}: {error}",
- args.file.display()
- ))
- })?;
- let context = mutation_validation_context(config)?;
- let canonical = canonicalize_draft(&parsed, &contents, &context).map_err(|error| {
- let issue = match error {
- ListingDraftValidationError::MissingSellerAccount(issue) => {
- return account::AccountRuntimeFailure::unresolved_with_detail(
- format!("{} ({})", issue.message, issue.field),
- json!({
- "seller_actor_source": "listing_draft",
- "listing_file": args.file.display().to_string(),
- "actions": listing_bound_account_recovery_actions(args.file.as_path()),
- }),
- )
- .into();
- }
- ListingDraftValidationError::Issue(issue) => issue,
- };
- RuntimeError::Config(format!(
- "invalid listing draft {}: {} ({})",
- args.file.display(),
- issue.message,
- issue.field
- ))
- })?;
- ensure_listing_bound_account(config, &canonical, args.file.as_path())?;
- let actor = Actor::from_public_key_hex(
- canonical.seller_pubkey.as_str(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Seller],
- )
- .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?;
- let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone());
- Ok(SdkListingPublishInput {
- canonical,
- actor,
- document,
- })
-}
-
-fn sdk_prepared_publish_view(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: &CanonicalListingDraft,
- plan: ListingPlan,
-) -> ListingMutationView {
- let listing_addr = plan.address().as_str().to_owned();
- let event = sdk_plan_event_view(&plan);
- ListingMutationView {
- state: "dry_run".to_owned(),
- operation: operation.as_str().to_owned(),
- source: SDK_LISTING_WRITE_SOURCE.to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_account_id: canonical.seller_account_id.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- seller_actor_source: canonical.seller_actor_source.clone(),
- event_kind: KIND_CLASSIFIED_LISTING,
- dry_run: true,
- deduplicated: false,
- target_transport_endpoints: Vec::new(),
- attempted_transport_endpoints: Vec::new(),
- accepted_transport_endpoints: Vec::new(),
- failed_transport_targets: Vec::new(),
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id: Some(plan.draft().expected_event_id().to_string()),
- event_addr: Some(listing_addr),
- idempotency_key: args.idempotency_key.clone(),
- local_replica: None,
- reason: Some("dry run requested; SDK enqueue and transport push skipped".to_owned()),
- job: None,
- event: args.print_event.then_some(event),
- actions: vec![format!("radroots listing publish {}", args.file.display())],
- }
-}
-
-fn sdk_plan_event_view(plan: &ListingPlan) -> ListingMutationEventView {
- ListingMutationEventView {
- kind: plan.draft().kind_u32(),
- author: plan.draft().expected_pubkey().to_hex(),
- created_at: Some(plan.draft().created_at_u64()),
- content: plan.draft().content().to_owned(),
- tags: plan.draft().tags_as_vec(),
- event_id: Some(plan.draft().expected_event_id().to_string()),
- signature: None,
- event_addr: plan.address().as_str().to_owned(),
- }
-}
-
-pub fn update(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- mutate(config, args, ListingMutationOperation::Update)
-}
-
-pub fn pause(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- mutate(config, args, ListingMutationOperation::Pause)
-}
-
-pub fn withdraw(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- mutate(config, args, ListingMutationOperation::Withdraw)
-}
-
-fn mutate(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- let contents = fs::read_to_string(&args.file).map_err(RuntimeError::from)?;
- let parsed = toml::from_str::<ListingDraftDocument>(&contents).map_err(|error| {
- RuntimeError::Config(format!(
- "invalid listing draft {}: {error}",
- args.file.display()
- ))
- })?;
- let context = mutation_validation_context(config)?;
- let mut canonical = canonicalize_draft(&parsed, &contents, &context).map_err(|error| {
- let issue = match error {
- ListingDraftValidationError::MissingSellerAccount(issue) => {
- return account::AccountRuntimeFailure::unresolved_with_detail(
- format!("{} ({})", issue.message, issue.field),
- json!({
- "seller_actor_source": "listing_draft",
- "listing_file": args.file.display().to_string(),
- "actions": listing_bound_account_recovery_actions(args.file.as_path()),
- }),
- )
- .into();
- }
- ListingDraftValidationError::Issue(issue) => issue,
- };
- RuntimeError::Config(format!(
- "invalid listing draft {}: {} ({})",
- args.file.display(),
- issue.message,
- issue.field
- ))
- })?;
- ensure_listing_bound_account(config, &canonical, args.file.as_path())?;
-
- if let Some(status) = operation.listing_status() {
- canonical.listing.availability = Some(OperationalListingAvailability::Status {
- status: OperationalListingStatus::Other {
- value: status.to_owned(),
- },
- });
- }
-
- if config.output.dry_run {
- validate_configured_listing_signer(config, &canonical)?;
- }
-
- mutate_via_sdk_from_canonical(config, args, operation, canonical)
-}
-
-fn mutate_via_sdk_from_canonical(
- config: &RuntimeConfig,
- args: &ListingMutationArgs,
- operation: ListingMutationOperation,
- canonical: CanonicalListingDraft,
-) -> Result<ListingMutationView, CliSdkAdapterError> {
- let actor = Actor::from_public_key_hex(
- canonical.seller_pubkey.as_str(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Seller],
- )
- .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?;
- let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone());
- let request = if matches!(operation, ListingMutationOperation::Publish) {
- sdk_listing::PrepareRequest::publish(actor, document, sdk_created_at_unix()?)
- } else {
- sdk_listing::PrepareRequest::update(actor, document, sdk_created_at_unix()?)
- };
- let plan = sdk_listing::prepare(request)
- .map_err(|error| RuntimeError::Config(format!("invalid SDK listing plan: {error}")))?;
- if !config.output.dry_run {
- return Err(RuntimeError::Config(
- "listing commit is unavailable until the shared sync engine is configured".to_owned(),
- )
- .into());
- }
- Ok(sdk_prepared_publish_view(
- config, args, operation, &canonical, plan,
- ))
-}
-
-fn sdk_created_at_unix() -> Result<u64, RuntimeError> {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_secs())
- .map_err(|error| RuntimeError::Config(format!("system clock error: {error}")))
-}
-
-fn scaffold_contents(draft: &ListingDraftDocument) -> Result<String, RuntimeError> {
- let toml = toml::to_string_pretty(draft).map_err(|error| {
- RuntimeError::Config(format!("failed to render listing draft: {error}"))
- })?;
- Ok(format!(
- "# radroots listing draft v1\n# this scaffold applies selected farm defaults and provided product inputs when available\n# review any remaining empty fields, then run `radroots listing publish <file>`\n\n{toml}"
- ))
-}
-
-fn validation_context(config: &RuntimeConfig) -> Result<ListingValidationContext, RuntimeError> {
- Ok(ListingValidationContext {
- farm_setup_action: farm_setup_action(config)?,
- })
-}
-
-fn mutation_validation_context(
- config: &RuntimeConfig,
-) -> Result<ListingValidationContext, RuntimeError> {
- validation_context(config)
-}
-
-fn canonicalize_draft(
- draft: &ListingDraftDocument,
- contents: &str,
- _context: &ListingValidationContext,
-) -> Result<CanonicalListingDraft, ListingDraftValidationError> {
- if draft.version != 1 {
- return Err(issue_for_field(
- contents,
- "version",
- format!("unsupported listing draft version `{}`", draft.version),
- )
- .into());
- }
- if draft.kind.trim() != DRAFT_KIND {
- return Err(issue_for_field(
- contents,
- "kind",
- format!("unsupported listing draft kind `{}`", draft.kind),
- )
- .into());
- }
-
- let listing_id = draft.listing.d_tag.trim().to_owned();
- if !is_d_tag_base64url(&listing_id) {
- return Err(issue_for_field(
- contents,
- "listing.d_tag",
- "listing d_tag must be a 22-character base64url identifier",
- )
- .into());
- }
-
- let seller_account_id =
- if let Some(account_id) = non_empty(draft.seller_actor.account_id.clone()) {
- account_id
- } else {
- return Err(ListingDraftValidationError::MissingSellerAccount(
- issue_for_field(
- contents,
- "seller_actor.account_id",
- "missing listing seller_actor account_id",
- ),
- ));
- };
-
- let seller_pubkey = if let Some(pubkey) = non_empty(draft.seller_actor.pubkey.clone()) {
- pubkey
- } else {
- return Err(ListingDraftValidationError::MissingSellerAccount(
- issue_for_field(
- contents,
- "seller_actor.pubkey",
- "missing listing seller_actor pubkey",
- ),
- ));
- };
-
- let seller_actor_source = if let Some(source) = non_empty(draft.seller_actor.source.clone()) {
- source
- } else {
- return Err(ListingDraftValidationError::MissingSellerAccount(
- issue_for_field(
- contents,
- "seller_actor.source",
- "missing listing seller_actor source",
- ),
- ));
- };
- if !matches!(
- seller_actor_source.as_str(),
- LISTING_SELLER_ACTOR_SOURCE_FARM_CONFIG
- | LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT
- | LISTING_SELLER_ACTOR_SOURCE_REBIND
- ) {
- return Err(issue_for_field(
- contents,
- "seller_actor.source",
- format!("unsupported listing seller_actor source `{seller_actor_source}`"),
- )
- .into());
- }
-
- let farm_d_tag = if let Some(d_tag) = non_empty(draft.listing.farm_d_tag.clone()) {
- d_tag
- } else {
- return Err(
- issue_for_field(contents, "listing.farm_d_tag", "missing listing farm_d_tag").into(),
- );
- };
- if !is_d_tag_base64url(&farm_d_tag) {
- return Err(issue_for_field(
- contents,
- "listing.farm_d_tag",
- "farm_d_tag must be a 22-character base64url identifier",
- )
- .into());
- }
-
- let quantity_amount = parse_decimal_field(
- draft.primary_bin.quantity_amount.as_str(),
- contents,
- "primary_bin.quantity_amount",
- )?;
- let quantity_unit = parse_unit_field(
- draft.primary_bin.quantity_unit.as_str(),
- contents,
- "primary_bin.quantity_unit",
- )?;
- let quantity = Quantity::try_new(quantity_amount, quantity_unit)
- .map_err(|error| {
- issue_for_field(
- contents,
- "primary_bin.quantity_amount",
- format!("invalid primary_bin quantity: {error}"),
- )
- })?
- .with_optional_label(non_empty(draft.primary_bin.label.clone()))
- .to_canonical()
- .map_err(|error| {
- issue_for_field(
- contents,
- "primary_bin.quantity_unit",
- format!("invalid primary_bin quantity unit conversion: {error}"),
- )
- })?;
-
- let price_amount = parse_decimal_field(
- draft.primary_bin.price_amount.as_str(),
- contents,
- "primary_bin.price_amount",
- )?;
- let price_currency = parse_currency_field(
- draft.primary_bin.price_currency.as_str(),
- contents,
- "primary_bin.price_currency",
- )?;
- let price_per_amount = parse_decimal_field(
- draft.primary_bin.price_per_amount.as_str(),
- contents,
- "primary_bin.price_per_amount",
- )?;
- let price_per_unit = parse_unit_field(
- draft.primary_bin.price_per_unit.as_str(),
- contents,
- "primary_bin.price_per_unit",
- )?;
- let price_money = Money::try_new(price_amount, price_currency).map_err(|error| {
- issue_for_field(contents, "primary_bin.price_amount", error.to_string())
- })?;
- let price_quantity = Quantity::try_new(price_per_amount, price_per_unit).map_err(|error| {
- issue_for_field(contents, "primary_bin.price_per_amount", error.to_string())
- })?;
- let price = QuantityPrice::try_new(price_money, price_quantity)
- .map_err(|error| issue_for_field(contents, "primary_bin.price_amount", error.to_string()))?
- .try_to_canonical_unit_price()
- .map_err(|error| {
- issue_for_field(
- contents,
- "primary_bin.price_per_unit",
- format!("invalid primary_bin price definition: {error:?}"),
- )
- })?;
-
- let inventory_available = parse_decimal_field(
- draft.inventory.available.as_str(),
- contents,
- "inventory.available",
- )?;
- let availability = build_availability(draft, contents)?;
- let delivery_method = build_delivery_method(draft, contents)?;
- let location = build_location(draft);
- let discounts = build_listing_discounts(
- draft,
- contents,
- draft.primary_bin.bin_id.trim(),
- price_currency,
- )?;
- let primary_bin_id =
- protocol_inventory_bin_id(draft.primary_bin.bin_id.trim(), "primary_bin.bin_id").map_err(
- |error| {
- issue_for_field(
- contents,
- "primary_bin.bin_id",
- format!("invalid primary_bin bin id: {error}"),
- )
- },
- )?;
-
- let listing = OperationalListing {
- d_tag: protocol_d_tag(listing_id.as_str(), "listing d_tag").map_err(|error| {
- issue_for_field(
- contents,
- "listing.d_tag",
- format!("invalid listing d_tag: {error}"),
- )
- })?,
- published_at: None,
- farm: FarmRef {
- pubkey: seller_pubkey.clone(),
- d_tag: farm_d_tag.clone(),
- },
- product: OperationalListingProduct {
- key: draft.product.key.trim().to_owned(),
- title: draft.product.title.trim().to_owned(),
- category: draft.product.category.trim().to_owned(),
- summary: non_empty(draft.product.summary.clone()),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: primary_bin_id.clone(),
- bins: vec![OperationalListingBin {
- bin_id: primary_bin_id,
- quantity,
- price_per_canonical_unit: price,
- display_amount: None,
- display_unit: None,
- display_label: non_empty(draft.primary_bin.label.clone()),
- display_price: None,
- display_price_unit: None,
- }],
- resource_area: None,
- plot: None,
- discounts,
- inventory_available: Some(inventory_available),
- availability: Some(availability),
- delivery_method: Some(delivery_method),
- location: Some(location),
- images: None,
- };
-
- Ok(CanonicalListingDraft {
- listing_id,
- seller_account_id,
- seller_pubkey,
- seller_actor_source,
- farm_d_tag,
- listing,
- })
-}
-
-fn build_availability(
- draft: &ListingDraftDocument,
- contents: &str,
-) -> Result<OperationalListingAvailability, ListingValidationIssueView> {
- let kind = if draft.availability.kind.trim().is_empty() {
- if draft.availability.start.is_some() || draft.availability.end.is_some() {
- "window"
- } else {
- "status"
- }
- } else {
- draft.availability.kind.trim()
- };
-
- match kind {
- "status" => {
- let status = draft.availability.status.trim();
- if status.is_empty() {
- return Err(issue_for_field(
- contents,
- "availability.status",
- "missing availability status",
- ));
- }
- Ok(OperationalListingAvailability::Status {
- status: match status {
- "active" => OperationalListingStatus::Active,
- "sold" => OperationalListingStatus::Sold,
- other => OperationalListingStatus::Other {
- value: other.to_owned(),
- },
- },
- })
- }
- "window" => Ok(OperationalListingAvailability::Window {
- start: draft.availability.start,
- end: draft.availability.end,
- }),
- _ => Err(issue_for_field(
- contents,
- "availability.kind",
- format!("unsupported availability kind `{kind}`"),
- )),
- }
-}
-
-fn build_delivery_method(
- draft: &ListingDraftDocument,
- contents: &str,
-) -> Result<OperationalListingDeliveryMethod, ListingValidationIssueView> {
- let method = draft.delivery.method.trim();
- if method.is_empty() {
- return Err(issue_for_field(
- contents,
- "delivery.method",
- "missing delivery method",
- ));
- }
-
- Ok(match method {
- "pickup" => OperationalListingDeliveryMethod::Pickup,
- "local_delivery" => OperationalListingDeliveryMethod::LocalDelivery,
- "shipping" => OperationalListingDeliveryMethod::Shipping,
- other => OperationalListingDeliveryMethod::Other {
- method: other.to_owned(),
- },
- })
-}
-
-fn build_location(draft: &ListingDraftDocument) -> OperationalListingPublicLocation {
- OperationalListingPublicLocation {
- primary: draft.location.primary.trim().to_owned(),
- city: draft.location.city.clone().and_then(non_empty),
- region: draft.location.region.clone().and_then(non_empty),
- country: draft.location.country.clone().and_then(non_empty),
- geohash: draft.location.geohash.trim().to_owned(),
- }
-}
-
-fn build_listing_discounts(
- draft: &ListingDraftDocument,
- contents: &str,
- primary_bin_id: &str,
- price_currency: Currency,
-) -> Result<Option<Vec<Discount>>, ListingValidationIssueView> {
- let mut discounts = Vec::new();
- for (index, discount) in draft.discounts.iter().enumerate() {
- let field_prefix = format!("discounts.{index}");
- if discount.id.trim().is_empty() {
- return Err(issue_for_field(
- contents,
- field_prefix.as_str(),
- "discount id must not be empty",
- ));
- }
- let bin_id = discount
- .bin_id
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .unwrap_or(primary_bin_id)
- .to_owned();
- let min = discount.min_bin_count.unwrap_or(1);
- if min == 0 {
- return Err(issue_for_field(
- contents,
- field_prefix.as_str(),
- "discount min_bin_count must be greater than zero",
- ));
- }
- let value = match discount.kind.trim() {
- "percent" => {
- let raw = discount.value.trim();
- if raw.is_empty() {
- return Err(issue_for_field(
- contents,
- field_prefix.as_str(),
- "percent discount requires value",
- ));
- }
- let percent = raw.parse::<Percent>().map_err(|error| {
- issue_for_field(
- contents,
- field_prefix.as_str(),
- format!("percent discount value is invalid: {error}"),
- )
- })?;
- DiscountValue::Percent(percent)
- }
- "amount" => {
- let raw_amount = discount.amount.trim();
- if raw_amount.is_empty() {
- return Err(issue_for_field(
- contents,
- field_prefix.as_str(),
- "amount discount requires amount",
- ));
- }
- let amount = parse_decimal_field(raw_amount, contents, field_prefix.as_str())?;
- let currency = if discount.currency.trim().is_empty() {
- price_currency
- } else {
- parse_currency_field(
- discount.currency.as_str(),
- contents,
- field_prefix.as_str(),
- )?
- };
- DiscountValue::MoneyPerBin(Money::try_new(amount, currency).map_err(|error| {
- issue_for_field(contents, field_prefix.as_str(), error.to_string())
- })?)
- }
- other => {
- return Err(issue_for_field(
- contents,
- field_prefix.as_str(),
- format!("unsupported discount kind `{other}`"),
- ));
- }
- };
- let discount = Discount::try_new(
- DiscountScope::Bin,
- DiscountThreshold::BinCount { bin_id, min },
- value,
- )
- .map_err(|error| issue_for_field(contents, field_prefix.as_str(), error.to_string()))?;
- discounts.push(discount);
- }
- Ok((!discounts.is_empty()).then_some(discounts))
-}
-
-fn listing_bound_account_issue(
- config: &RuntimeConfig,
- canonical: &CanonicalListingDraft,
- contents: &str,
-) -> Result<Option<ListingValidationIssueView>, RuntimeError> {
- let Some(account) = configured_account(config, &canonical.seller_account_id)? else {
- return Ok(Some(issue_for_field(
- contents,
- "seller_actor.account_id",
- format!(
- "listing seller_actor account_id `{}` is not present in the local account store",
- canonical.seller_account_id
- ),
- )));
- };
- let account_pubkey = account.record.public_identity().public_key().to_hex();
- if !account_pubkey.eq_ignore_ascii_case(canonical.seller_pubkey.as_str()) {
- return Ok(Some(issue_for_field(
- contents,
- "seller_actor.pubkey",
- format!(
- "listing seller_actor pubkey `{}` does not match account `{}` pubkey `{account_pubkey}`",
- canonical.seller_pubkey, canonical.seller_account_id
- ),
- )));
- }
- Ok(None)
-}
-
-fn ensure_listing_bound_account(
- config: &RuntimeConfig,
- canonical: &CanonicalListingDraft,
- file: &Path,
-) -> Result<(), RuntimeError> {
- validate_invocation_account_matches_bound(config, canonical, file)?;
- let Some(account) = configured_account(config, &canonical.seller_account_id)? else {
- return Err(account::AccountRuntimeFailure::unresolved_with_detail(
- format!(
- "listing-bound seller account `{}` is not present in the local account store",
- canonical.seller_account_id
- ),
- json!({
- "seller_actor_source": canonical.seller_actor_source,
- "listing_seller_account_id": canonical.seller_account_id,
- "listing_file": file.display().to_string(),
- "actions": listing_bound_account_recovery_actions(file),
- }),
- )
- .into());
- };
- let account_pubkey = account.record.public_identity().public_key().to_hex();
- if !account_pubkey.eq_ignore_ascii_case(canonical.seller_pubkey.as_str()) {
- return Err(account::AccountRuntimeFailure::mismatch_with_detail(
- format!(
- "account mismatch: listing-bound seller account `{}` pubkey `{account_pubkey}` cannot sign listing seller_pubkey `{}`",
- canonical.seller_account_id, canonical.seller_pubkey
- ),
- json!({
- "seller_actor_source": canonical.seller_actor_source,
- "listing_seller_account_id": canonical.seller_account_id,
- "listing_seller_pubkey": canonical.seller_pubkey,
- "account_pubkey": account_pubkey,
- "listing_file": file.display().to_string(),
- "actions": listing_bound_account_recovery_actions(file),
- }),
- )
- .into());
- }
- Ok(())
-}
-
-fn validate_invocation_account_matches_bound(
- config: &RuntimeConfig,
- canonical: &CanonicalListingDraft,
- file: &Path,
-) -> Result<(), RuntimeError> {
- let Some(selector) = config
- .account
- .selector
- .as_deref()
- .map(str::trim)
- .filter(|selector| !selector.is_empty())
- else {
- return Ok(());
- };
- let attempted = account::resolve_account_selector(config, selector)?;
- if attempted.record.id().to_string() == canonical.seller_account_id {
- return Ok(());
- }
- Err(account::AccountRuntimeFailure::mismatch_with_detail(
- format!(
- "account mismatch: listing draft is bound to seller account `{}`; invocation selected `{}`",
- canonical.seller_account_id, attempted.record.id()
- ),
- json!({
- "seller_actor_source": canonical.seller_actor_source,
- "listing_seller_account_id": canonical.seller_account_id,
- "attempted_seller_account_id": attempted.record.id().to_string(),
- "listing_file": file.display().to_string(),
- "actions": listing_bound_account_recovery_actions(file),
- }),
- )
- .into())
-}
-
-fn listing_bound_account_recovery_actions(file: &Path) -> Vec<String> {
- vec![
- "radroots account import <path>".to_owned(),
- format!("radroots listing rebind {} <selector>", file.display()),
- ]
-}
-
-fn invalid_validation_view(
- file: &Path,
- draft: &ListingDraftDocument,
- context: &ListingValidationContext,
- issue: ListingValidationIssueView,
-) -> ListingValidateView {
- let mut actions = vec![format!("edit {}", file.display())];
- if draft.seller_actor.account_id.trim().is_empty() {
- actions.push("radroots account create".to_owned());
- } else {
- actions.push(format!(
- "radroots listing rebind {} <selector>",
- file.display()
- ));
- }
- if draft.listing.farm_d_tag.trim().is_empty() {
- actions.push(context.farm_setup_action.clone());
- }
-
- ListingValidateView {
- state: "invalid".to_owned(),
- source: LISTING_SOURCE.to_owned(),
- file: file.display().to_string(),
- valid: false,
- listing_id: non_empty(draft.listing.d_tag.clone()),
- seller_account_id: non_empty(draft.seller_actor.account_id.clone()),
- seller_pubkey: non_empty(draft.seller_actor.pubkey.clone()),
- seller_actor_source: non_empty(draft.seller_actor.source.clone()),
- farm_d_tag: non_empty(draft.listing.farm_d_tag.clone()),
- issues: vec![issue],
- actions,
- }
-}
-
-fn validate_configured_listing_signer(
- config: &RuntimeConfig,
- canonical: &CanonicalListingDraft,
-) -> Result<(), RuntimeError> {
- validate_configured_signer_for_actor(
- config,
- Some(canonical.seller_account_id.as_str()),
- canonical.seller_pubkey.as_str(),
- "listing seller",
- )
-}
-
-fn validate_operational_listing_draft(
- canonical: &CanonicalListingDraft,
-) -> Result<(), OperationalListingValidationError> {
- let seller_pubkey = PublicKey::from_hex(canonical.seller_pubkey.as_str())
- .map_err(|_| OperationalListingValidationError::InvalidSeller)?;
- validate_operational_listing_model(canonical.listing.clone(), &seller_pubkey).map(|_| ())
-}
-
-fn issue_from_trade_validation(
- error: OperationalListingValidationError,
- contents: &str,
-) -> ListingValidationIssueView {
- match error {
- OperationalListingValidationError::InvalidSeller => issue_for_field(
- contents,
- "seller_actor.pubkey",
- "listing author does not match the farm pubkey",
- ),
- OperationalListingValidationError::MissingTitle => {
- issue_for_field(contents, "product.title", "missing listing title")
- }
- OperationalListingValidationError::MissingDescription => {
- issue_for_field(contents, "product.summary", "missing listing description")
- }
- OperationalListingValidationError::MissingProductType => {
- issue_for_field(contents, "product.category", "missing listing product type")
- }
- OperationalListingValidationError::MissingBins
- | OperationalListingValidationError::MissingPrimaryBin
- | OperationalListingValidationError::InvalidBin => {
- issue_for_field(contents, "primary_bin.bin_id", error.to_string())
- }
- OperationalListingValidationError::MissingPrice
- | OperationalListingValidationError::InvalidPrice => issue_for_field(
- contents,
- "primary_bin.price_amount",
- "invalid listing price",
- ),
- OperationalListingValidationError::MissingInventory
- | OperationalListingValidationError::InvalidInventory => {
- issue_for_field(contents, "inventory.available", error.to_string())
- }
- OperationalListingValidationError::MissingAvailability => issue_for_field(
- contents,
- "availability.status",
- "missing listing availability",
- ),
- OperationalListingValidationError::MissingLocation
- | OperationalListingValidationError::MissingLocationLocality => {
- issue_for_field(contents, "location.primary", error.to_string())
- }
- OperationalListingValidationError::MissingLocationGeohash
- | OperationalListingValidationError::InvalidLocationGeohash => {
- issue_for_field(contents, "location.geohash", error.to_string())
- }
- OperationalListingValidationError::MissingDeliveryMethod => issue_for_field(
- contents,
- "delivery.method",
- "missing listing delivery method",
- ),
- other => issue_for_field(contents, "listing", other.to_string()),
- }
-}
-
-fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults, RuntimeError> {
- let account_resolution = account::resolve_account_resolution(config)?;
- let Some(selected_account) = account_resolution.resolved_account.clone() else {
- return Err(account::AccountRuntimeFailure::unresolved_with_detail(
- "no resolved account is available for listing seller actor",
- json!({
- "seller_actor_source": LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT,
- "actions": [
- "radroots account create",
- "radroots account import <path>",
- ],
- }),
- )
- .into());
- };
- let mut defaults = ListingAuthoringDefaults {
- farm_config_present: false,
- farm_defaults_ready: false,
- farm_next_action: Some(farm_setup_action(config)?),
- farm_reason: Some(
- "selected farm draft not found; delivery, location, and farm defaults were left blank"
- .to_owned(),
- ),
- farm_name: None,
- seller_account_id: selected_account.record.id().to_string(),
- seller_pubkey: selected_account
- .record
- .public_identity()
- .public_key()
- .to_hex(),
- seller_actor_source: LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT.to_owned(),
- selected_farm_d_tag: None,
- delivery_method: None,
- location: None,
- };
-
- let Some(resolved) = farm_config::load(config, None)? else {
- return Ok(defaults);
- };
- let Some(account) = configured_account(config, &resolved.document.selection.account)? else {
- let account_id = resolved.document.selection.account.clone();
- return Err(account::AccountRuntimeFailure::unresolved_with_detail(
- format!(
- "farm-bound seller account `{account_id}` is not present in the local account store"
- ),
- json!({
- "seller_actor_source": "farm_config",
- "farm_bound_seller_account_id": account_id,
- "actions": crate::runtime::farm::farm_bound_seller_recovery_actions("<selector>"),
- }),
- )
- .into());
- };
-
- defaults.farm_config_present = true;
- defaults.farm_name = resolved
- .document
- .profile
- .display_name
- .clone()
- .and_then(non_empty)
- .or_else(|| non_empty(resolved.document.profile.name.clone()))
- .or_else(|| non_empty(resolved.document.farm.name.clone()));
- defaults.seller_account_id = resolved.document.selection.account.clone();
- defaults.seller_pubkey = account.record.public_identity().public_key().to_hex();
- defaults.seller_actor_source = LISTING_SELLER_ACTOR_SOURCE_FARM_CONFIG.to_owned();
- defaults.selected_farm_d_tag = Some(resolved.document.selection.farm_d_tag.clone());
- let draft_missing = farm_config::missing_fields(&resolved.document);
- defaults.farm_defaults_ready = !draft_missing.iter().any(|field| {
- matches!(
- field,
- farm_config::FarmMissingField::Location
- | farm_config::FarmMissingField::City
- | farm_config::FarmMissingField::Delivery
- | farm_config::FarmMissingField::Geohash
- )
- });
- if defaults.farm_defaults_ready {
- defaults.delivery_method = Some(resolved.document.listing_defaults.delivery_method.clone());
- defaults.location = Some(draft_location_from_model(
- &resolved.document.listing_defaults.location,
- ));
- defaults.farm_next_action = None;
- defaults.farm_reason = None;
- } else {
- defaults.farm_next_action = Some("radroots farm get".to_owned());
- defaults.farm_reason = Some(
- "selected farm draft is missing delivery or location defaults; those fields were left blank"
- .to_owned(),
- );
- }
- Ok(defaults)
-}
-
-fn draft_location_from_model(location: &OperationalListingPublicLocation) -> ListingDraftLocation {
- ListingDraftLocation {
- primary: location.primary.clone(),
- city: location.city.clone(),
- region: location.region.clone(),
- country: location.country.clone(),
- geohash: location.geohash.clone(),
- }
-}
-
-fn farm_setup_action(_config: &RuntimeConfig) -> Result<String, RuntimeError> {
- Ok("radroots farm create".to_owned())
-}
-
-fn drafts_dir(config: &RuntimeConfig) -> PathBuf {
- config.paths.app_data_root.join(LISTING_DRAFTS_DIR)
-}
-
-fn file_stem(path: &Path) -> String {
- path.file_stem()
- .and_then(|value| value.to_str())
- .unwrap_or("unknown")
- .to_owned()
-}
-
-fn modified_unix(path: &Path) -> Option<u64> {
- let modified = fs::metadata(path).ok()?.modified().ok()?;
- modified
- .duration_since(UNIX_EPOCH)
- .ok()
- .map(|value| value.as_secs())
-}
-
-fn configured_account(
- config: &RuntimeConfig,
- account_id: &str,
-) -> Result<Option<account::AccountRecordView>, RuntimeError> {
- let snapshot = account::snapshot(config)?;
- Ok(snapshot
- .accounts
- .into_iter()
- .find(|account| account.record.id().to_hex() == account_id))
-}
-
-fn parse_decimal_field(
- value: &str,
- contents: &str,
- field: &str,
-) -> Result<Decimal, ListingValidationIssueView> {
- value.trim().parse::<Decimal>().map_err(|_| {
- issue_for_field(
- contents,
- field,
- format!("`{field}` must be a valid decimal value"),
- )
- })
-}
-
-fn parse_unit_field(
- value: &str,
- contents: &str,
- field: &str,
-) -> Result<Unit, ListingValidationIssueView> {
- value.parse::<Unit>().map_err(|_| {
- issue_for_field(
- contents,
- field,
- format!("`{field}` must be a valid unit code"),
- )
- })
-}
-
-fn parse_currency_field(
- value: &str,
- contents: &str,
- field: &str,
-) -> Result<Currency, ListingValidationIssueView> {
- let upper = value.trim().to_ascii_uppercase();
- Currency::from_str_upper(&upper).map_err(|_| {
- issue_for_field(
- contents,
- field,
- format!("`{field}` must be a valid ISO currency code"),
- )
- })
-}
-
-fn issue_for_field(
- contents: &str,
- field: &str,
- message: impl Into<String>,
-) -> ListingValidationIssueView {
- ListingValidationIssueView {
- field: field.to_owned(),
- message: message.into(),
- line: line_for_field(contents, field),
- }
-}
-
-fn line_for_field(contents: &str, field: &str) -> Option<usize> {
- let needles: &[&str] = match field {
- "version" => &["version ="],
- "kind" => &["kind ="],
- "listing.d_tag" => &["d_tag ="],
- "listing.farm_d_tag" => &["farm_d_tag ="],
- "seller_actor.account_id" => &["[seller_actor]", "account_id ="],
- "seller_actor.pubkey" => &["[seller_actor]", "pubkey ="],
- "seller_actor.source" => &["[seller_actor]", "source ="],
- "product.key" => &["key ="],
- "product.title" => &["title ="],
- "product.category" => &["category ="],
- "product.summary" => &["summary ="],
- "primary_bin.bin_id" => &["bin_id ="],
- "primary_bin.quantity_amount" => &["quantity_amount ="],
- "primary_bin.quantity_unit" => &["quantity_unit ="],
- "primary_bin.price_amount" => &["price_amount ="],
- "primary_bin.price_currency" => &["price_currency ="],
- "primary_bin.price_per_amount" => &["price_per_amount ="],
- "primary_bin.price_per_unit" => &["price_per_unit ="],
- "inventory.available" => &["available ="],
- "availability.kind" => &["[availability]", "kind ="],
- "availability.status" => &["status ="],
- "delivery.method" => &["method ="],
- "location.primary" => &["primary ="],
- field if field.starts_with("discounts.") => &["[[discounts]]"],
- _ => &[],
- };
- for needle in needles {
- if let Some(line) = contents.lines().position(|line| line.contains(needle)) {
- return Some(line + 1);
- }
- }
- None
-}
-
-fn line_for_offset(contents: &str, offset: usize) -> usize {
- let mut seen = 0usize;
- for (index, line) in contents.lines().enumerate() {
- seen += line.len() + 1;
- if seen >= offset {
- return index + 1;
- }
- }
- contents.lines().count().max(1)
-}
-
-fn non_empty(value: String) -> Option<String> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- None
- } else {
- Some(trimmed.to_owned())
- }
-}
-
-fn generate_d_tag() -> String {
- let nanos = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_nanos())
- .unwrap_or_default();
- let counter = D_TAG_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
- let mixed = nanos ^ counter;
- encode_base64url_no_pad(mixed.to_be_bytes())
-}
-
-fn encode_base64url_no_pad(bytes: [u8; 16]) -> String {
- const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
- let mut output = String::with_capacity(22);
- let mut index = 0usize;
- while index + 3 <= bytes.len() {
- let block = ((bytes[index] as u32) << 16)
- | ((bytes[index + 1] as u32) << 8)
- | (bytes[index + 2] as u32);
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
- output.push(ALPHABET[(block & 0x3f) as usize] as char);
- index += 3;
- }
- let remaining = bytes.len() - index;
- if remaining == 1 {
- let block = (bytes[index] as u32) << 16;
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- } else if remaining == 2 {
- let block = ((bytes[index] as u32) << 16) | ((bytes[index + 1] as u32) << 8);
- output.push(ALPHABET[((block >> 18) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 12) & 0x3f) as usize] as char);
- output.push(ALPHABET[((block >> 6) & 0x3f) as usize] as char);
- }
- output
-}
-
-#[cfg(test)]
-mod tests {
- use super::{DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag};
- use radroots_event_codec::d_tag::is_d_tag_base64url;
-
- #[test]
- fn generated_listing_d_tag_is_valid_base64url() {
- let d_tag = generate_d_tag();
- assert!(is_d_tag_base64url(&d_tag));
- }
-
- #[test]
- fn base64url_encoder_produces_twenty_two_characters_for_sixteen_bytes() {
- let encoded = encode_base64url_no_pad([0u8; 16]);
- assert_eq!(encoded.len(), 22);
- assert!(is_d_tag_base64url(&encoded));
- }
-
- #[test]
- fn listing_draft_kind_constant_is_stable() {
- let document = ListingDraftDocument {
- version: 1,
- kind: DRAFT_KIND.to_owned(),
- listing: super::ListingDraftMeta {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_owned(),
- farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_owned(),
- },
- seller_actor: super::ListingDraftSellerActor {
- account_id: "acct_seller".to_owned(),
- pubkey: "a".repeat(64),
- source: super::LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT.to_owned(),
- },
- product: super::ListingDraftProduct {
- key: "sku".to_owned(),
- title: "Widget".to_owned(),
- category: "produce".to_owned(),
- summary: "Fresh".to_owned(),
- },
- primary_bin: super::ListingDraftPrimaryBin {
- bin_id: "bin-1".to_owned(),
- quantity_amount: "1".to_owned(),
- quantity_unit: "kg".to_owned(),
- price_amount: "12.50".to_owned(),
- price_currency: "USD".to_owned(),
- price_per_amount: "1".to_owned(),
- price_per_unit: "kg".to_owned(),
- label: "kg".to_owned(),
- },
- inventory: super::ListingDraftInventory {
- available: "2".to_owned(),
- },
- availability: super::ListingDraftAvailability {
- kind: "status".to_owned(),
- status: "active".to_owned(),
- start: None,
- end: None,
- },
- delivery: super::ListingDraftDelivery {
- method: "pickup".to_owned(),
- },
- location: super::ListingDraftLocation {
- primary: "Asheville".to_owned(),
- city: None,
- region: None,
- country: None,
- geohash: "dnqwy".to_owned(),
- },
- discounts: Vec::new(),
- };
- let rendered = toml::to_string_pretty(&document).expect("render draft");
- assert!(rendered.contains("kind = \"listing_draft_v1\""));
- }
-
- #[test]
- fn listing_draft_canonicalization_preserves_discounts_and_validates_semantics() {
- let seller_pubkey = "a".repeat(64);
- let document = ListingDraftDocument {
- version: 1,
- kind: DRAFT_KIND.to_owned(),
- listing: super::ListingDraftMeta {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_owned(),
- farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_owned(),
- },
- seller_actor: super::ListingDraftSellerActor {
- account_id: "acct_seller".to_owned(),
- pubkey: seller_pubkey.clone(),
- source: super::LISTING_SELLER_ACTOR_SOURCE_RESOLVED_ACCOUNT.to_owned(),
- },
- product: super::ListingDraftProduct {
- key: "sku".to_owned(),
- title: "Widget".to_owned(),
- category: "produce".to_owned(),
- summary: "Fresh".to_owned(),
- },
- primary_bin: super::ListingDraftPrimaryBin {
- bin_id: "bin-1".to_owned(),
- quantity_amount: "1".to_owned(),
- quantity_unit: "each".to_owned(),
- price_amount: "10".to_owned(),
- price_currency: "USD".to_owned(),
- price_per_amount: "1".to_owned(),
- price_per_unit: "each".to_owned(),
- label: "each".to_owned(),
- },
- inventory: super::ListingDraftInventory {
- available: "2".to_owned(),
- },
- availability: super::ListingDraftAvailability {
- kind: "status".to_owned(),
- status: "active".to_owned(),
- start: None,
- end: None,
- },
- delivery: super::ListingDraftDelivery {
- method: "pickup".to_owned(),
- },
- location: super::ListingDraftLocation {
- primary: "Farm stand".to_owned(),
- city: Some("Asheville".to_owned()),
- region: None,
- country: None,
- geohash: "dnqwy".to_owned(),
- },
- discounts: vec![super::ListingDraftDiscount {
- id: "discount_farmstand".to_owned(),
- label: "farmstand pickup".to_owned(),
- kind: "percent".to_owned(),
- value: "10".to_owned(),
- amount: String::new(),
- currency: String::new(),
- bin_id: None,
- min_bin_count: None,
- }],
- };
- let contents = toml::to_string_pretty(&document).expect("render draft");
- let context = super::ListingValidationContext {
- farm_setup_action: "radroots farm create".to_owned(),
- };
-
- let canonical =
- super::canonicalize_draft(&document, contents.as_str(), &context).expect("canonical");
-
- assert!(contents.contains("[[discounts]]"));
- assert_eq!(
- canonical
- .listing
- .discounts
- .as_ref()
- .expect("discounts")
- .len(),
- 1
- );
- super::validate_operational_listing_draft(&canonical)
- .expect("canonical listing passes operational validation");
-
- let mut missing_description = canonical;
- missing_description.listing.product.summary = Some(" ".to_owned());
- assert_eq!(
- super::validate_operational_listing_draft(&missing_description),
- Err(super::OperationalListingValidationError::MissingDescription)
- );
- }
-}
diff --git a/src/runtime/logging.rs b/src/runtime/logging.rs
@@ -1,57 +0,0 @@
-use std::path::PathBuf;
-
-use radroots_log::{LogFileLayout, LoggingOptions};
-
-use crate::runtime::config::LoggingConfig;
-
-const CLI_LOG_FILE_NAME: &str = "radroots-cli.log";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct LoggingState {
- pub initialized: bool,
- pub current_file: Option<PathBuf>,
-}
-
-pub fn initialize_logging(config: &LoggingConfig) -> Result<LoggingState, radroots_log::Error> {
- let options = to_radroots_logging_options(config);
- let state = LoggingState {
- initialized: true,
- current_file: options.resolved_current_log_file_path(),
- };
- radroots_log::init_logging(options)?;
- Ok(state)
-}
-
-pub fn to_radroots_logging_options(config: &LoggingConfig) -> LoggingOptions {
- LoggingOptions {
- dir: config.directory.clone(),
- file_name: CLI_LOG_FILE_NAME.to_owned(),
- stdout: config.stdout,
- default_level: Some(config.filter.clone()),
- file_layout: LogFileLayout::StableFileName,
- ..LoggingOptions::default()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{LogFileLayout, to_radroots_logging_options};
- use crate::runtime::config::LoggingConfig;
- use std::path::PathBuf;
-
- #[test]
- fn logging_options_use_cli_file_name() {
- let options = to_radroots_logging_options(&LoggingConfig {
- filter: "info".to_owned(),
- directory: Some(PathBuf::from("logs")),
- stdout: false,
- });
- assert_eq!(options.file_name, "radroots-cli.log");
- assert_eq!(options.default_level.as_deref(), Some("info"));
- assert_eq!(options.dir, Some(PathBuf::from("logs")));
- assert!(!options.stdout);
- assert_eq!(options.file_layout, LogFileLayout::StableFileName);
- assert!(options.rotation.max_file_bytes > 0);
- assert!(options.rotation.retained_files > 0);
- }
-}
diff --git a/src/runtime/mesh.rs b/src/runtime/mesh.rs
@@ -1,96 +0,0 @@
-use radroots_mesh::{
- RADROOTS_MESH_UNAVAILABLE_MESSAGE, RadrootsMeshAdmissionInput, RadrootsMeshPayloadPolicy,
- RadrootsMeshPrivacyClass, RadrootsMeshScope,
-};
-use serde_json::Value as JsonValue;
-use toml::{Value, map::Map};
-
-use crate::ops::OperationData;
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::transport::update_app_config_table;
-use crate::view::runtime::{MeshPolicyCheckView, MeshScopeView, MeshStatusView};
-
-const MESH_SOURCE: &str = "mesh config";
-
-pub fn scope(config: &RuntimeConfig) -> MeshScopeView {
- scope_view(config.mesh.scope.as_str(), "ready")
-}
-
-pub fn set_scope(
- config: &RuntimeConfig,
- input: &OperationData,
-) -> Result<MeshScopeView, RuntimeError> {
- let scope = input
- .get("scope")
- .and_then(JsonValue::as_str)
- .unwrap_or("disabled");
- match scope {
- "disabled" | "local" => {}
- other => {
- return Err(RuntimeError::Config(format!(
- "mesh scope `{other}` is not supported"
- )));
- }
- }
- let mut mesh = Map::new();
- mesh.insert("scope".to_owned(), Value::String(scope.to_owned()));
- update_app_config_table(config, "mesh", Value::Table(mesh))?;
- Ok(scope_view(scope, "configured"))
-}
-
-pub fn status(config: &RuntimeConfig) -> MeshStatusView {
- let scope = config.mesh.scope.as_str();
- MeshStatusView {
- state: "ready".to_owned(),
- source: MESH_SOURCE.to_owned(),
- scope: scope.to_owned(),
- transport: "reticulum".to_owned(),
- configured: scope != "disabled",
- implementation: "unavailable".to_owned(),
- usable_for_delivery: false,
- message: RADROOTS_MESH_UNAVAILABLE_MESSAGE.to_owned(),
- }
-}
-
-pub fn policy_check(config: &RuntimeConfig) -> MeshPolicyCheckView {
- let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable();
- let privacy_class = RadrootsMeshPrivacyClass::PublicEvent;
- let input = RadrootsMeshAdmissionInput::new(RadrootsMeshScope::Local, privacy_class, 1, 1);
- let decision = policy.evaluate(&input);
- let deny_reason = decision
- .deny_reason()
- .map(|reason| reason.label())
- .unwrap_or("none");
-
- MeshPolicyCheckView {
- state: "ready".to_owned(),
- source: MESH_SOURCE.to_owned(),
- scope: config.mesh.scope.as_str().to_owned(),
- policy: policy.policy_id().to_owned(),
- transport: "reticulum".to_owned(),
- usable_for_delivery: decision.usable_for_delivery(),
- decision: decision.label().to_owned(),
- deny_reason: deny_reason.to_owned(),
- privacy_class: privacy_class.label().to_owned(),
- payload_bytes: input.payload_bytes,
- frame_bytes: input.frame_bytes,
- max_payload_bytes: policy.max_payload_bytes,
- max_frame_bytes: policy.max_frame_bytes,
- compression: policy.compression.label().to_owned(),
- custom_scopes_enabled: policy.custom_scopes_enabled,
- message: decision.message().to_owned(),
- }
-}
-
-fn scope_view(scope: &str, state: &str) -> MeshScopeView {
- MeshScopeView {
- state: state.to_owned(),
- source: MESH_SOURCE.to_owned(),
- scope: scope.to_owned(),
- implementation: "unavailable".to_owned(),
- message: "Mesh delivery is disabled unless a Reticulum scope is explicitly configured"
- .to_owned(),
- actions: vec!["radroots diagnostics inspect".to_owned()],
- }
-}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -1,61 +0,0 @@
-pub mod account;
-pub mod config;
-pub mod farm;
-pub mod farm_config;
-pub mod find;
-pub mod hyf;
-pub mod listing;
-pub mod logging;
-pub mod mesh;
-pub mod paths;
-pub mod provider;
-pub mod runtime_store;
-pub mod sdk;
-pub mod signer;
-pub mod signing;
-pub mod store;
-pub mod sync;
-pub mod trade;
-pub mod transport;
-
-use std::process::ExitCode;
-
-#[derive(Debug, thiserror::Error)]
-pub enum RuntimeError {
- #[error("{0}")]
- Config(String),
- #[error("{0}")]
- Account(#[from] account::AccountRuntimeFailure),
- #[error("failed to initialize logging: {0}")]
- Logging(#[from] radroots_log::Error),
- #[error("accounts error: {0}")]
- Accounts(#[from] radroots_nostr_accounts::prelude::RadrootsNostrAccountsError),
- #[error("replica sql error: {0}")]
- Sql(#[from] radroots_replica_store::SqlError),
- #[error("replica sync error: {0}")]
- ReplicaSync(#[from] radroots_replica_sync::RadrootsReplicaEventsError),
- #[error("runtime store error: {0}")]
- RuntimeStore(#[from] radroots_runtime_store::RuntimeStoreError),
- #[error("network error: {0}")]
- Network(String),
- #[error("failed to serialize json output: {0}")]
- Json(#[from] serde_json::Error),
- #[error("failed to write output: {0}")]
- Io(#[from] std::io::Error),
-}
-
-impl RuntimeError {
- pub fn exit_code(&self) -> ExitCode {
- match self {
- Self::Config(_) | Self::Account(_) => ExitCode::from(2),
- Self::Logging(_)
- | Self::Accounts(_)
- | Self::Sql(_)
- | Self::ReplicaSync(_)
- | Self::RuntimeStore(_)
- | Self::Network(_)
- | Self::Json(_)
- | Self::Io(_) => ExitCode::from(1),
- }
- }
-}
diff --git a/src/runtime/paths.rs b/src/runtime/paths.rs
@@ -1,250 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use radroots_runtime_paths::{
- DEFAULT_CONFIG_FILE_NAME, RadrootsPathOverrides, RadrootsPathProfile, RadrootsRuntimeNamespace,
- default_shared_identity_path,
-};
-
-use crate::runtime::{
- RuntimeError,
- config::{EnvFileValues, Environment},
-};
-
-const CLI_DEFAULT_PROFILE: &str = "interactive_user";
-const CLI_REPO_LOCAL_PROFILE: &str = "repo_local";
-const CLI_APP_NAMESPACE_VALUE: &str = "cli";
-const SHARED_ACCOUNTS_NAMESPACE_VALUE: &str = "accounts";
-const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities";
-
-pub(crate) const CLI_ALLOWED_PROFILES: &[&str] = &[CLI_DEFAULT_PROFILE, CLI_REPO_LOCAL_PROFILE];
-pub(crate) const ENV_CLI_PATHS_PROFILE: &str = "RADROOTS_CLI_PATHS_PROFILE";
-pub(crate) const ENV_CLI_PATHS_REPO_LOCAL_ROOT: &str = "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT";
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct PathsConfig {
- pub profile: String,
- pub profile_source: String,
- pub allowed_profiles: Vec<String>,
- pub root_source: String,
- pub repo_local_root: Option<PathBuf>,
- pub repo_local_root_source: Option<String>,
- pub subordinate_path_override_source: String,
- pub app_namespace: String,
- pub shared_accounts_namespace: String,
- pub shared_identities_namespace: String,
- pub app_config_path: PathBuf,
- pub workspace_config_path: Option<PathBuf>,
- pub app_data_root: PathBuf,
- pub shared_cache_root: PathBuf,
- pub app_logs_root: PathBuf,
- pub shared_accounts_data_root: PathBuf,
- pub shared_accounts_secrets_root: PathBuf,
- pub default_identity_path: PathBuf,
-}
-
-pub(crate) fn resolve_paths(
- env: &dyn Environment,
- env_file: &EnvFileValues,
-) -> Result<PathsConfig, RuntimeError> {
- let current_dir = env.current_dir()?;
- let resolver = env.path_resolver();
- let (profile, profile_label, profile_source) = resolve_cli_path_profile(env, env_file)?;
- let override_selection =
- resolve_cli_path_overrides(current_dir.as_path(), env, env_file, profile)?;
- let workspace_config_path = resolve_workspace_config_path(profile, &override_selection)?;
- let resolved = resolver
- .resolve(profile, &override_selection.overrides)
- .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?;
- let app_namespace = RadrootsRuntimeNamespace::app(CLI_APP_NAMESPACE_VALUE)
- .map_err(|err| RuntimeError::Config(format!("resolve cli namespace: {err}")))?;
- let shared_accounts_namespace =
- RadrootsRuntimeNamespace::shared(SHARED_ACCOUNTS_NAMESPACE_VALUE).map_err(|err| {
- RuntimeError::Config(format!("resolve shared accounts namespace: {err}"))
- })?;
- let shared_identity_namespace =
- RadrootsRuntimeNamespace::shared(SHARED_IDENTITIES_NAMESPACE_VALUE).map_err(|err| {
- RuntimeError::Config(format!("resolve shared identities namespace: {err}"))
- })?;
- let app_paths = resolved.namespaced(&app_namespace);
- let shared_accounts_paths = resolved.namespaced(&shared_accounts_namespace);
- let default_identity_path =
- default_shared_identity_path(&resolver, profile, &override_selection.overrides)
- .map_err(|err| RuntimeError::Config(format!("resolve shared identity path: {err}")))?;
-
- Ok(PathsConfig {
- profile: profile_label.to_owned(),
- profile_source,
- allowed_profiles: CLI_ALLOWED_PROFILES
- .iter()
- .map(|value| (*value).to_owned())
- .collect(),
- root_source: path_root_source(profile).to_owned(),
- repo_local_root: override_selection.repo_local_root,
- repo_local_root_source: override_selection.repo_local_root_source,
- subordinate_path_override_source: "runtime_config".to_owned(),
- app_namespace: app_namespace.relative_path().display().to_string(),
- shared_accounts_namespace: shared_accounts_namespace
- .relative_path()
- .display()
- .to_string(),
- shared_identities_namespace: shared_identity_namespace
- .relative_path()
- .display()
- .to_string(),
- app_config_path: app_paths.config.join(DEFAULT_CONFIG_FILE_NAME),
- workspace_config_path,
- app_data_root: app_paths.data,
- shared_cache_root: resolved.cache,
- app_logs_root: app_paths.logs,
- shared_accounts_data_root: shared_accounts_paths.data,
- shared_accounts_secrets_root: shared_accounts_paths.secrets,
- default_identity_path,
- })
-}
-
-fn resolve_cli_path_profile(
- env: &dyn Environment,
- env_file: &EnvFileValues,
-) -> Result<(RadrootsPathProfile, &'static str, String), RuntimeError> {
- match path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_PROFILE]) {
- Some(entry) => parse_cli_path_profile(entry.key.as_str(), entry.value.as_str())
- .map(|(profile, label)| (profile, label, entry.source_label())),
- None => Ok((
- RadrootsPathProfile::InteractiveUser,
- CLI_DEFAULT_PROFILE,
- "default".to_owned(),
- )),
- }
-}
-
-fn parse_cli_path_profile(
- key: &str,
- value: &str,
-) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
- match value.trim().to_ascii_lowercase().as_str() {
- CLI_DEFAULT_PROFILE => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
- CLI_REPO_LOCAL_PROFILE => Ok((RadrootsPathProfile::RepoLocal, CLI_REPO_LOCAL_PROFILE)),
- other => Err(RuntimeError::Config(format!(
- "{key} must be `interactive_user` or `repo_local`, got `{other}`"
- ))),
- }
-}
-
-struct CliPathOverrideSelection {
- overrides: RadrootsPathOverrides,
- repo_local_root: Option<PathBuf>,
- repo_local_root_source: Option<String>,
-}
-
-fn resolve_cli_path_overrides(
- current_dir: &Path,
- env: &dyn Environment,
- env_file: &EnvFileValues,
- profile: RadrootsPathProfile,
-) -> Result<CliPathOverrideSelection, RuntimeError> {
- match profile {
- RadrootsPathProfile::InteractiveUser => Ok(CliPathOverrideSelection {
- overrides: RadrootsPathOverrides::default(),
- repo_local_root: None,
- repo_local_root_source: None,
- }),
- RadrootsPathProfile::RepoLocal => {
- let Some(entry) = path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_REPO_LOCAL_ROOT])
- else {
- return Err(RuntimeError::Config(format!(
- "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be set when {ENV_CLI_PATHS_PROFILE}=repo_local"
- )));
- };
- if entry.value.trim().is_empty() {
- return Err(RuntimeError::Config(format!(
- "{} must not be empty when {ENV_CLI_PATHS_PROFILE}=repo_local",
- entry.key
- )));
- }
- let repo_local_root = normalize_explicit_path_root(current_dir, entry.value.as_str());
- Ok(CliPathOverrideSelection {
- overrides: RadrootsPathOverrides::repo_local(repo_local_root.as_path()),
- repo_local_root: Some(repo_local_root),
- repo_local_root_source: Some(entry.source_label()),
- })
- }
- _ => Err(RuntimeError::Config(
- "cli only supports interactive_user and repo_local path profiles".to_owned(),
- )),
- }
-}
-
-fn path_root_source(profile: RadrootsPathProfile) -> &'static str {
- match profile {
- RadrootsPathProfile::InteractiveUser => "host_defaults",
- RadrootsPathProfile::RepoLocal => "repo_local_root",
- RadrootsPathProfile::ServiceHost => "service_host_defaults",
- RadrootsPathProfile::MobileNative => "mobile_native_defaults",
- }
-}
-
-fn normalize_explicit_path_root(current_dir: &Path, value: &str) -> PathBuf {
- let root = PathBuf::from(value.trim());
- if root.is_absolute() {
- root
- } else {
- current_dir.join(root)
- }
-}
-
-fn resolve_workspace_config_path(
- profile: RadrootsPathProfile,
- override_selection: &CliPathOverrideSelection,
-) -> Result<Option<PathBuf>, RuntimeError> {
- match profile {
- RadrootsPathProfile::InteractiveUser => Ok(None),
- RadrootsPathProfile::RepoLocal => override_selection
- .repo_local_root
- .as_ref()
- .map(|root| Some(root.join(DEFAULT_CONFIG_FILE_NAME)))
- .ok_or_else(|| {
- RuntimeError::Config(format!(
- "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be resolved when {ENV_CLI_PATHS_PROFILE}=repo_local"
- ))
- }),
- _ => Err(RuntimeError::Config(
- "cli only supports interactive_user and repo_local path profiles".to_owned(),
- )),
- }
-}
-
-struct PathEnvValueEntry {
- key: String,
- value: String,
- source_kind: &'static str,
-}
-
-impl PathEnvValueEntry {
- fn source_label(&self) -> String {
- format!("{}:{}", self.source_kind, self.key)
- }
-}
-
-fn path_env_value_entry(
- env: &dyn Environment,
- env_file: &EnvFileValues,
- keys: &[&str],
-) -> Option<PathEnvValueEntry> {
- keys.iter()
- .find_map(|key| {
- env.var(key).map(|value| PathEnvValueEntry {
- key: (*key).to_owned(),
- value,
- source_kind: "process_env",
- })
- })
- .or_else(|| {
- keys.iter().find_map(|key| {
- env_file.get(key).map(|value| PathEnvValueEntry {
- key: (*key).to_owned(),
- value: value.to_owned(),
- source_kind: "env_file",
- })
- })
- })
-}
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -1,458 +0,0 @@
-use crate::runtime::config::RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE;
-#[cfg(test)]
-use crate::runtime::config::{
- CapabilityBindingInspection, CapabilityBindingInspectionState, INFERENCE_HYF_STDIO_CAPABILITY,
-};
-use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
-#[cfg(test)]
-use crate::runtime::hyf;
-use crate::view::runtime::PublishRuntimeView;
-
-#[cfg(test)]
-const WRITE_PLANE_TARGET_DETAIL: &str =
- "write-plane targets are resolved by mode-specific publish commands";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ProviderProvenance {
- #[cfg(test)]
- ExplicitBinding,
- #[cfg(test)]
- ManagedDefault,
- #[cfg(test)]
- DirectConfig,
- #[cfg(test)]
- Disabled,
- TransportProfile,
- #[cfg(test)]
- Unavailable,
-}
-
-impl ProviderProvenance {
- pub fn as_str(self) -> &'static str {
- match self {
- #[cfg(test)]
- Self::ExplicitBinding => "explicit_binding",
- #[cfg(test)]
- Self::ManagedDefault => "managed_default",
- #[cfg(test)]
- Self::DirectConfig => "direct_config",
- #[cfg(test)]
- Self::Disabled => "disabled",
- Self::TransportProfile => "transport_profile",
- #[cfg(test)]
- Self::Unavailable => "unavailable",
- }
- }
-}
-
-#[cfg(test)]
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResolvedProviderView {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- pub target_kind: Option<String>,
- pub target: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WritePlaneProviderView {
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- pub target_kind: Option<String>,
- pub target: Option<String>,
- pub detail: String,
-}
-
-#[cfg(test)]
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct ResolvedWritePlaneTarget {
- pub url: String,
-}
-
-#[cfg(test)]
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct HyfProviderView {
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- pub target_kind: Option<String>,
- pub target: Option<String>,
- pub executable: String,
- pub reason: Option<String>,
- pub protocol_version: Option<u64>,
- pub deterministic_available: Option<bool>,
-}
-
-pub fn resolve_write_plane_provider(
- config: &RuntimeConfig,
- publish: &PublishRuntimeView,
-) -> WritePlaneProviderView {
- let (provider_runtime_id, binding_model, detail) = match config.transport.profile {
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget => (
- config.transport.profile.as_str(),
- "nostr_transport",
- "configured transport profile publishes through Nostr; readiness is reported under publish",
- ),
- TransportProfileKind::LocalOnly => (
- "local_only",
- "local_transport",
- "local_only transport profile does not provide network publish",
- ),
- TransportProfileKind::Reticulum => (
- "reticulum",
- "reticulum_transport",
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
- ),
- };
- WritePlaneProviderView {
- provider_runtime_id: provider_runtime_id.to_owned(),
- binding_model: binding_model.to_owned(),
- state: publish.state.clone(),
- provenance: ProviderProvenance::TransportProfile.as_str().to_owned(),
- source: publish.source.clone(),
- target_kind: None,
- target: None,
- detail: publish.reason.clone().unwrap_or_else(|| detail.to_owned()),
- }
-}
-
-#[cfg(test)]
-pub fn resolve_actor_write_plane_target(
- config: &RuntimeConfig,
-) -> Result<ResolvedWritePlaneTarget, String> {
- let _ = config;
- Err(WRITE_PLANE_TARGET_DETAIL.to_owned())
-}
-
-#[cfg(test)]
-pub fn resolve_hyf_provider(config: &RuntimeConfig) -> HyfProviderView {
- let binding = inspect_binding(config, INFERENCE_HYF_STDIO_CAPABILITY);
- let status = hyf::resolve_runtime_status(config);
- let binding_configured = binding.state == CapabilityBindingInspectionState::Configured;
- let provenance = if binding_configured {
- binding_provenance(&binding)
- } else if status.state == "disabled" {
- ProviderProvenance::Disabled
- } else {
- ProviderProvenance::DirectConfig
- }
- .as_str()
- .to_owned();
- let target_kind = hyf_target_kind(config, &binding);
- let target = hyf_target(config, &binding);
- let executable = hyf_executable(config, &binding, &status);
- let source = if binding_configured {
- binding.source.clone()
- } else {
- status.source.clone()
- };
-
- HyfProviderView {
- provider_runtime_id: binding.provider_runtime_id,
- binding_model: binding.binding_model,
- state: status.state,
- provenance,
- source,
- target_kind,
- target,
- executable,
- reason: status.reason,
- protocol_version: status.protocol_version,
- deterministic_available: status.deterministic_available,
- }
-}
-
-#[cfg(test)]
-pub fn resolve_capability_providers(config: &RuntimeConfig) -> Vec<ResolvedProviderView> {
- let hyf = resolve_hyf_provider(config);
-
- vec![ResolvedProviderView {
- capability_id: INFERENCE_HYF_STDIO_CAPABILITY.to_owned(),
- provider_runtime_id: hyf.provider_runtime_id,
- binding_model: hyf.binding_model,
- state: hyf.state,
- provenance: hyf.provenance,
- source: hyf.source,
- target_kind: hyf.target_kind,
- target: hyf.target,
- }]
-}
-
-#[cfg(test)]
-fn inspect_binding(config: &RuntimeConfig, capability_id: &str) -> CapabilityBindingInspection {
- config
- .inspect_capability_bindings()
- .into_iter()
- .find(|binding| binding.capability_id == capability_id)
- .expect("provider capability binding inspection must exist")
-}
-
-#[cfg(test)]
-fn binding_provenance(binding: &CapabilityBindingInspection) -> ProviderProvenance {
- match binding.state {
- CapabilityBindingInspectionState::Configured => match binding.target_kind.as_deref() {
- Some("managed_instance") => ProviderProvenance::ManagedDefault,
- _ => ProviderProvenance::ExplicitBinding,
- },
- CapabilityBindingInspectionState::Disabled => ProviderProvenance::Disabled,
- CapabilityBindingInspectionState::NotConfigured => ProviderProvenance::Unavailable,
- }
-}
-
-#[cfg(test)]
-fn hyf_target_kind(
- config: &RuntimeConfig,
- binding: &CapabilityBindingInspection,
-) -> Option<String> {
- if binding.state == CapabilityBindingInspectionState::Configured {
- return binding.target_kind.clone();
- }
- if config.hyf.enabled {
- return Some("direct_config".to_owned());
- }
- None
-}
-
-#[cfg(test)]
-fn hyf_target(config: &RuntimeConfig, binding: &CapabilityBindingInspection) -> Option<String> {
- if binding.state == CapabilityBindingInspectionState::Configured {
- return binding.target.clone();
- }
- if config.hyf.enabled {
- return Some(config.hyf.executable.display().to_string());
- }
- None
-}
-
-#[cfg(test)]
-fn hyf_executable(
- config: &RuntimeConfig,
- binding: &CapabilityBindingInspection,
- status: &hyf::HyfStatusView,
-) -> String {
- if binding.state == CapabilityBindingInspectionState::Configured
- && binding.target_kind.as_deref() == Some("explicit_endpoint")
- {
- return binding
- .target
- .clone()
- .unwrap_or_else(|| status.executable.clone());
- }
- if !config.hyf.enabled {
- return status.executable.clone();
- }
- status.executable.clone()
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_secret_vault::RadrootsSecretBackend;
- use std::path::PathBuf;
-
- use super::{
- ProviderProvenance, resolve_actor_write_plane_target, resolve_capability_providers,
- resolve_hyf_provider, resolve_write_plane_provider,
- };
- use crate::runtime::config::{
- AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
- CapabilityBindingSource, CapabilityBindingTargetKind, HyfConfig, IdentityConfig,
- InteractionConfig, LocalConfig, LoggingConfig, MycConfig, OutputConfig, OutputFormat,
- PathsConfig, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
- };
- use crate::view::runtime::{
- PublishProviderRuntimeView, PublishRelayRuntimeView, PublishRuntimeView,
- };
-
- fn sample_config(bindings: Vec<CapabilityBindingConfig>, hyf_enabled: bool) -> RuntimeConfig {
- RuntimeConfig {
- output: OutputConfig {
- format: OutputFormat::Terminal,
- verbosity: Verbosity::Normal,
- dry_run: false,
- },
- interaction: InteractionConfig {
- input_enabled: true,
- assume_yes: false,
- stdin_tty: true,
- stdout_tty: true,
- prompts_allowed: true,
- confirmations_allowed: true,
- },
- paths: PathsConfig {
- profile: "interactive_user".into(),
- profile_source: "default".into(),
- allowed_profiles: vec!["interactive_user".into()],
- root_source: "host_defaults".into(),
- repo_local_root: None,
- repo_local_root_source: None,
- subordinate_path_override_source: "runtime_config".into(),
- app_namespace: "apps/cli".into(),
- shared_accounts_namespace: "shared/accounts".into(),
- shared_identities_namespace: "shared/identities".into(),
- app_config_path: PathBuf::from("/tmp/config/apps/cli/config.toml"),
- workspace_config_path: None,
- app_data_root: PathBuf::from("/tmp/data"),
- shared_cache_root: PathBuf::from("/tmp/cache"),
- app_logs_root: PathBuf::from("/tmp/logs"),
- shared_accounts_data_root: PathBuf::from("/tmp/shared/accounts"),
- shared_accounts_secrets_root: PathBuf::from("/tmp/shared/accounts-secrets"),
- default_identity_path: PathBuf::from("/tmp/default-identity.json"),
- },
- logging: LoggingConfig {
- filter: "info".into(),
- directory: None,
- stdout: true,
- },
- account: AccountConfig {
- selector: None,
- store_path: PathBuf::from("/tmp/store.json"),
- secrets_dir: PathBuf::from("/tmp/secrets"),
- secret_backend: RadrootsSecretBackend::EncryptedFile,
- },
- account_secret_contract: AccountSecretContractConfig {
- default_backend: "host_vault".into(),
- allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
- host_vault_policy: Some("desktop".into()),
- uses_protected_store: true,
- },
- identity: IdentityConfig {
- path: PathBuf::from("/tmp/default-identity.json"),
- },
- signer: SignerConfig {
- backend: SignerBackend::Local,
- },
- transport: crate::runtime::config::TransportConfig::local_only(),
- local: LocalConfig {
- root: PathBuf::from("/tmp/local"),
- replica_store_path: PathBuf::from("/tmp/local/replica.sqlite"),
- backups_dir: PathBuf::from("/tmp/local/backups"),
- exports_dir: PathBuf::from("/tmp/local/exports"),
- },
- myc: MycConfig {
- executable: PathBuf::from("myc"),
- status_timeout_ms: 2_000,
- },
- hyf: HyfConfig {
- enabled: hyf_enabled,
- executable: PathBuf::from("hyfd"),
- },
- mesh: crate::runtime::config::MeshConfig::disabled(),
- rpc: RpcConfig {
- url: "http://127.0.0.1:7070".into(),
- },
- rhi: crate::runtime::config::RhiConfig {
- validator_set: None,
- require_cryptographic_proof: false,
- },
- capability_bindings: bindings,
- }
- }
-
- fn publish_view(
- config: &RuntimeConfig,
- state: &str,
- reason: Option<&str>,
- ) -> PublishRuntimeView {
- PublishRuntimeView {
- transport: config.transport.profile.as_str().to_owned(),
- source: config.transport.source.as_str().to_owned(),
- transport_family: config.transport.profile.transport_family().to_owned(),
- state: state.to_owned(),
- executable: state == "ready",
- reason: reason.map(str::to_owned),
- signed_write_required: true,
- relay: PublishRelayRuntimeView {
- ready: !config.transport.nostr_relay_urls.is_empty(),
- count: config.transport.nostr_relay_urls.len(),
- source: config.transport.source.as_str().to_owned(),
- },
- provider: PublishProviderRuntimeView {
- provider_runtime_id: config.transport.profile.as_str().to_owned(),
- state: state.to_owned(),
- source: config.transport.source.as_str().to_owned(),
- reason: reason.map(str::to_owned),
- },
- }
- }
-
- #[test]
- fn write_plane_provider_tracks_nostr_transport_publish() {
- let mut config = sample_config(Vec::new(), false);
- config.transport = crate::runtime::config::TransportConfig::from_nostr_relay_urls(vec![
- "wss://relay.example.com".to_owned(),
- ]);
- let publish = publish_view(
- &config,
- "ready",
- Some("Nostr relay transport is configured for delivery"),
- );
- let view = resolve_write_plane_provider(&config, &publish);
- assert_eq!(view.provider_runtime_id, "nostr");
- assert_eq!(view.binding_model, "nostr_transport");
- assert_eq!(view.state, "ready");
- assert_eq!(
- view.provenance,
- ProviderProvenance::TransportProfile.as_str()
- );
- assert!(view.target.is_none());
- assert!(view.detail.contains("configured for delivery"));
- }
-
- #[test]
- fn actor_write_plane_target_fails_closed() {
- let error = resolve_actor_write_plane_target(&sample_config(Vec::new(), false))
- .expect_err("write plane target");
- assert_eq!(
- error,
- "write-plane targets are resolved by mode-specific publish commands"
- );
- }
-
- #[test]
- fn hyf_uses_direct_config_when_enabled_without_binding() {
- let view = resolve_hyf_provider(&sample_config(Vec::new(), true));
- assert_eq!(view.provenance, ProviderProvenance::DirectConfig.as_str());
- assert_eq!(view.target_kind.as_deref(), Some("direct_config"));
- assert_eq!(view.target.as_deref(), Some("hyfd"));
- }
-
- #[test]
- fn hyf_binding_remains_visible_when_runtime_is_disabled() {
- let binding = CapabilityBindingConfig {
- capability_id: "inference.hyf_stdio".into(),
- provider_runtime_id: "hyf".into(),
- binding_model: "stdio_service".into(),
- source: CapabilityBindingSource::UserConfig,
- target_kind: CapabilityBindingTargetKind::ExplicitEndpoint,
- target: "bin/hyfd-user".into(),
- managed_account_ref: None,
- signer_session_ref: None,
- };
- let view = resolve_hyf_provider(&sample_config(vec![binding], false));
- assert_eq!(view.state, "disabled");
- assert_eq!(
- view.provenance,
- ProviderProvenance::ExplicitBinding.as_str()
- );
- assert_eq!(view.source, "user config [[capability_binding]]");
- assert_eq!(view.target_kind.as_deref(), Some("explicit_endpoint"));
- assert_eq!(view.target.as_deref(), Some("bin/hyfd-user"));
- assert_eq!(view.executable, "bin/hyfd-user");
- }
-
- #[test]
- fn capability_provider_list_only_covers_active_hyf_provider() {
- let providers = resolve_capability_providers(&sample_config(Vec::new(), false));
- assert_eq!(providers.len(), 1);
- assert_eq!(providers[0].capability_id, "inference.hyf_stdio");
- }
-}
diff --git a/src/runtime/runtime_store.rs b/src/runtime/runtime_store.rs
@@ -1,193 +0,0 @@
-use std::fs;
-use std::path::PathBuf;
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_runtime_paths::{
- default_shared_runtime_store_database_path_from_shared_accounts_data_root,
- default_shared_runtime_store_root_from_shared_accounts_data_root,
-};
-use radroots_runtime_store::{
- PublishOutboxStatus, RuntimeStore, RuntimeStoreRecord, RuntimeStoreRecordFamily,
- RuntimeStoreRecordInput, RuntimeStoreRecordStatus, SourceRuntime,
-};
-use radroots_sql_core::SqlxSqliteExecutor;
-use serde_json::Value;
-
-use crate::runtime::RuntimeError;
-use crate::runtime::config::{PathsConfig, RuntimeConfig};
-
-static RECORD_COUNTER: AtomicU64 = AtomicU64::new(0);
-
-pub fn append_local_work(
- config: &RuntimeConfig,
- subject: &str,
- owner_account_id: Option<String>,
- owner_pubkey: Option<String>,
- farm_id: Option<String>,
- listing_addr: Option<String>,
- payload: Value,
-) -> Result<RuntimeStoreRecord, RuntimeError> {
- let timestamp = current_time_ms()?;
- let sequence = RECORD_COUNTER.fetch_add(1, Ordering::Relaxed);
- let input = RuntimeStoreRecordInput {
- record_id: format!("cli:local_work:{subject}:{timestamp}:{sequence}"),
- family: RuntimeStoreRecordFamily::LocalWork,
- status: RuntimeStoreRecordStatus::LocalSaved,
- source_runtime: SourceRuntime::Cli,
- created_at_ms: timestamp,
- inserted_at_ms: timestamp,
- owner_account_id,
- owner_pubkey,
- farm_id,
- listing_addr,
- local_work_json: Some(payload),
- event_id: None,
- event_kind: None,
- event_pubkey: None,
- event_created_at: None,
- event_tags_json: None,
- event_content: None,
- event_sig: None,
- raw_event_json: None,
- outbox_status: PublishOutboxStatus::None,
- relay_set_fingerprint: None,
- relay_delivery_json: None,
- };
- let store = open_store(config)?;
- Ok(store.append_record(&input)?)
-}
-
-pub fn shared_runtime_store_db_path(config: &RuntimeConfig) -> Result<PathBuf, RuntimeError> {
- shared_runtime_store_db_path_from_paths(&config.paths)
-}
-
-fn shared_runtime_store_db_path_from_paths(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> {
- default_shared_runtime_store_database_path_from_shared_accounts_data_root(
- &paths.shared_accounts_data_root,
- )
- .map_err(|err| {
- RuntimeError::Config(format!("resolve shared runtime-store database path: {err}"))
- })
-}
-
-pub fn list_shared_records_latest(
- config: &RuntimeConfig,
- limit: u32,
-) -> Result<Vec<RuntimeStoreRecord>, RuntimeError> {
- let database_path = shared_runtime_store_db_path(config)?;
- if !database_path.exists() {
- return Ok(Vec::new());
- }
- let executor = SqlxSqliteExecutor::open(database_path)?;
- let store = RuntimeStore::new(executor);
- Ok(store.list_records_changed_latest(limit)?)
-}
-
-pub fn list_shared_records_before(
- config: &RuntimeConfig,
- before_change_seq: i64,
- before_seq: i64,
- limit: u32,
-) -> Result<Vec<RuntimeStoreRecord>, RuntimeError> {
- let database_path = shared_runtime_store_db_path(config)?;
- if !database_path.exists() {
- return Ok(Vec::new());
- }
- let executor = SqlxSqliteExecutor::open(database_path)?;
- let store = RuntimeStore::new(executor);
- Ok(store.list_records_changed_before(before_change_seq, before_seq, limit)?)
-}
-
-pub fn get_shared_record(
- config: &RuntimeConfig,
- record_id: &str,
-) -> Result<Option<RuntimeStoreRecord>, RuntimeError> {
- let database_path = shared_runtime_store_db_path(config)?;
- if !database_path.exists() {
- return Ok(None);
- }
- let executor = SqlxSqliteExecutor::open(database_path)?;
- let store = RuntimeStore::new(executor);
- Ok(store.get_record(record_id)?)
-}
-
-fn open_store(config: &RuntimeConfig) -> Result<RuntimeStore<SqlxSqliteExecutor>, RuntimeError> {
- let root = shared_runtime_store_root_from_paths(&config.paths)?;
- fs::create_dir_all(&root)?;
- let executor =
- SqlxSqliteExecutor::open(shared_runtime_store_db_path_from_paths(&config.paths)?)?;
- let store = RuntimeStore::new(executor);
- store.migrate_up()?;
- Ok(store)
-}
-
-fn shared_runtime_store_root_from_paths(paths: &PathsConfig) -> Result<PathBuf, RuntimeError> {
- default_shared_runtime_store_root_from_shared_accounts_data_root(
- &paths.shared_accounts_data_root,
- )
- .map_err(|err| RuntimeError::Config(format!("resolve shared runtime-store root: {err}")))
-}
-
-fn current_time_ms() -> Result<i64, RuntimeError> {
- let duration = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|error| {
- RuntimeError::Config(format!("system clock is before unix epoch: {error}"))
- })?;
- i64::try_from(duration.as_millis())
- .map_err(|_| RuntimeError::Config("current timestamp exceeds i64 milliseconds".to_owned()))
-}
-
-#[cfg(test)]
-mod tests {
- use std::path::PathBuf;
-
- use super::{shared_runtime_store_db_path_from_paths, shared_runtime_store_root_from_paths};
- use crate::runtime::config::PathsConfig;
-
- #[test]
- fn shared_runtime_store_paths_use_shared_runtime_contract() {
- let paths = paths_config("/repo/infra/local/runtime/radroots/data/shared/accounts");
-
- assert_eq!(
- shared_runtime_store_root_from_paths(&paths).expect("shared runtime-store root"),
- PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/runtime_store")
- );
- assert_eq!(
- shared_runtime_store_db_path_from_paths(&paths).expect("shared runtime-store database"),
- PathBuf::from(
- "/repo/infra/local/runtime/radroots/data/shared/runtime_store/runtime_store.sqlite"
- )
- );
- }
-
- fn paths_config(shared_accounts_data_root: &str) -> PathsConfig {
- PathsConfig {
- profile: "repo_local".to_owned(),
- profile_source: "test".to_owned(),
- allowed_profiles: vec!["repo_local".to_owned()],
- root_source: "repo_local_root".to_owned(),
- repo_local_root: Some(PathBuf::from("/repo/infra/local/runtime/radroots")),
- repo_local_root_source: Some("test".to_owned()),
- subordinate_path_override_source: "runtime_config".to_owned(),
- app_namespace: "apps/cli".to_owned(),
- shared_accounts_namespace: "shared/accounts".to_owned(),
- shared_identities_namespace: "shared/identities".to_owned(),
- app_config_path: PathBuf::from(
- "/repo/infra/local/runtime/radroots/config/apps/cli/config.toml",
- ),
- workspace_config_path: None,
- app_data_root: PathBuf::from("/repo/infra/local/runtime/radroots/data/apps/cli"),
- shared_cache_root: PathBuf::from("/repo/infra/local/runtime/radroots/cache"),
- app_logs_root: PathBuf::from("/repo/infra/local/runtime/radroots/logs/apps/cli"),
- shared_accounts_data_root: PathBuf::from(shared_accounts_data_root),
- shared_accounts_secrets_root: PathBuf::from(
- "/repo/infra/local/runtime/radroots/secrets/shared/accounts",
- ),
- default_identity_path: PathBuf::from(
- "/repo/infra/local/runtime/radroots/secrets/shared/identities/default.json",
- ),
- }
- }
-}
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -1,390 +0,0 @@
-//! CLI-owned composition of the final SDK capability graph.
-
-use std::{
- fs,
- future::Future,
- path::PathBuf,
- sync::Arc,
- time::{SystemTime, UNIX_EPOCH},
-};
-
-use radroots_sdk::{Client, ClientBuilder, Error as SdkError};
-use radroots_signing::{
- SignReceipt, SignRequest, Signer, SignerStatus, signer::BoxFuture as SigningFuture,
-};
-use radroots_storage::{event::SourceGeneration, memory::MemoryStorage};
-use radroots_storage_sqlite::{OpenMode, OpenOptions, Paths, SqliteStorage};
-use radroots_sync::policy::{Clock, DeadlinePolicy, IdSource, OperationKind, SyncId, SyncStorage};
-use radroots_transport::{
- BoxFuture, DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, EventSource,
- FetchPage, FetchRequest, SinkStatus, SourceStatus, TransportId,
- capability::{Availability, Maturity, SinkCapabilities, SourceCapabilities},
-};
-use radroots_transport_nostr::{Config as NostrConfig, NostrTransport, RelayUrlPolicy};
-use tokio::runtime::{Builder as TokioRuntimeBuilder, Runtime};
-
-use crate::runtime::{
- RuntimeError,
- config::{RuntimeConfig, TransportProfileKind},
- signing,
-};
-
-const SDK_STORAGE_DIR_NAME: &str = "sdk";
-const SYNC_TIMEOUT_MS: u64 = 30_000;
-const LOCAL_TRANSPORT_MESSAGE: &str =
- "local-only transport is configured without network fetch or delivery";
-
-pub(crate) use signing::{MYC_NIP46_SESSION_SECRET_SERVICE, myc_managed_account_ref_matches};
-
-#[derive(Debug, thiserror::Error)]
-pub enum CliSdkAdapterError {
- #[error("{0}")]
- Runtime(#[from] RuntimeError),
- #[error("{0}")]
- Sdk(#[from] SdkError),
- #[error("{0}")]
- Sync(#[from] radroots_sync::Error),
- #[error("{0}")]
- Storage(#[from] radroots_storage_sqlite::Error),
- #[error("{0}")]
- Transport(#[from] radroots_transport_nostr::Error),
- #[error("{0}")]
- Io(#[from] std::io::Error),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CliSdkConfig {
- pub storage_root: PathBuf,
-}
-
-impl CliSdkConfig {
- pub fn from_runtime_config(config: &RuntimeConfig) -> Self {
- Self {
- storage_root: sdk_storage_root(config),
- }
- }
-
- fn sqlite_options(&self) -> Result<OpenOptions, CliSdkAdapterError> {
- fs::create_dir_all(&self.storage_root)?;
- let paths = Paths::from_directory(&self.storage_root)?;
- let mut options = OpenOptions::new(paths, OpenMode::Create);
- if !self.storage_root.join("runtime.sqlite").exists() {
- options = options.with_source_generation(new_source_generation()?, now_unix_ms()?)?;
- }
- Ok(options)
- }
-}
-
-pub struct CliSdkSession {
- runtime: Runtime,
- sdk: Client,
- config: CliSdkConfig,
-}
-
-impl CliSdkSession {
- pub fn connect(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> {
- Self::connect_inner(config, None, false)
- }
-
- pub fn connect_storage_status(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> {
- Self::connect(config)
- }
-
- pub fn connect_memory(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> {
- Self::connect_inner(config, None, true)
- }
-
- pub fn connect_for_actor(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
- actor_label: &str,
- ) -> Result<Self, CliSdkAdapterError> {
- let runtime = sdk_runtime()?;
- let provider = runtime.block_on(signing::provider_for_actor(
- config,
- actor_account_id,
- actor_pubkey,
- actor_label,
- ))?;
- Self::compose(config, runtime, Some(provider), false)
- }
-
- pub fn connect_memory_for_actor(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
- actor_label: &str,
- ) -> Result<Self, CliSdkAdapterError> {
- let runtime = sdk_runtime()?;
- let provider = runtime.block_on(signing::provider_for_actor(
- config,
- actor_account_id,
- actor_pubkey,
- actor_label,
- ))?;
- Self::compose(config, runtime, Some(provider), true)
- }
-
- fn connect_inner(
- config: &RuntimeConfig,
- signer: Option<radroots_sdk::signing::Provider>,
- memory: bool,
- ) -> Result<Self, CliSdkAdapterError> {
- Self::compose(config, sdk_runtime()?, signer, memory)
- }
-
- fn compose(
- config: &RuntimeConfig,
- runtime: Runtime,
- signer: Option<radroots_sdk::signing::Provider>,
- memory: bool,
- ) -> Result<Self, CliSdkAdapterError> {
- let sdk_config = CliSdkConfig::from_runtime_config(config);
- if memory {
- let storage = Arc::new(MemoryStorage::new(new_source_generation()?));
- Self::compose_with_storage(config, runtime, sdk_config, signer, storage)
- } else {
- let storage =
- Arc::new(runtime.block_on(SqliteStorage::open(sdk_config.sqlite_options()?))?);
- Self::compose_with_storage(config, runtime, sdk_config, signer, storage)
- }
- }
-
- fn compose_with_storage<T>(
- config: &RuntimeConfig,
- runtime: Runtime,
- sdk_config: CliSdkConfig,
- signer: Option<radroots_sdk::signing::Provider>,
- storage: Arc<T>,
- ) -> Result<Self, CliSdkAdapterError>
- where
- T: radroots_storage::Storage + SyncStorage + 'static,
- {
- let (source, sink) = transport_capabilities(config)?;
- let signer_capability = signer
- .as_ref()
- .map(|provider| Arc::new(SharedProvider(provider.clone())) as Arc<dyn Signer>);
- let mut engine = radroots_sync::Engine::builder(
- storage.clone(),
- Arc::new(SystemClock),
- Arc::new(RandomIds),
- DeadlinePolicy::new(SYNC_TIMEOUT_MS, SYNC_TIMEOUT_MS, SYNC_TIMEOUT_MS)?,
- )
- .source(Arc::clone(&source))
- .sink(Arc::clone(&sink));
- if let Some(capability) = signer_capability.as_ref() {
- engine = engine.signer(Arc::clone(capability));
- }
- let engine = engine.build()?;
-
- let mut builder = ClientBuilder::new()
- .storage(storage)
- .source(source)
- .sink(sink)
- .sync_engine(engine);
- if let Some(provider) = signer {
- builder = builder.signing(provider);
- }
- let sdk = builder.build()?;
- Ok(Self {
- runtime,
- sdk,
- config: sdk_config,
- })
- }
-
- pub fn sdk(&self) -> &Client {
- &self.sdk
- }
-
- pub fn config(&self) -> &CliSdkConfig {
- &self.config
- }
-
- pub fn block_on<F: Future>(&self, future: F) -> F::Output {
- self.runtime.block_on(future)
- }
-}
-
-pub fn validate_configured_signer_for_actor(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
- actor_label: &str,
-) -> Result<(), RuntimeError> {
- signing::validate_for_actor(config, actor_account_id, actor_pubkey, actor_label)
-}
-
-pub fn sdk_storage_root(config: &RuntimeConfig) -> PathBuf {
- config.local.root.join(SDK_STORAGE_DIR_NAME)
-}
-
-pub(crate) fn sdk_runtime() -> Result<Runtime, RuntimeError> {
- TokioRuntimeBuilder::new_multi_thread()
- .enable_all()
- .build()
- .map_err(|error| {
- RuntimeError::Config(format!("failed to initialize SDK async runtime: {error}"))
- })
-}
-
-pub fn sdk_nostr_relay_url_policy(config: &RuntimeConfig) -> RelayUrlPolicy {
- if config
- .transport
- .nostr_relay_urls
- .iter()
- .any(|relay_url| relay_url.starts_with("ws://"))
- {
- RelayUrlPolicy::Local
- } else {
- RelayUrlPolicy::Public
- }
-}
-
-pub(crate) fn sync_targets(
- config: &RuntimeConfig,
-) -> Result<radroots_transport::TargetSet, TransportError> {
- let targets = config
- .transport
- .nostr_relay_urls
- .iter()
- .map(|relay| radroots_transport::Target::nostr_relay(relay))
- .collect::<Result<Vec<_>, _>>()?;
- radroots_transport::TargetSet::new(targets)
-}
-
-fn transport_capabilities(
- config: &RuntimeConfig,
-) -> Result<(Arc<dyn EventSource>, Arc<dyn EventSink>), CliSdkAdapterError> {
- if matches!(
- config.transport.profile,
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
- ) && !config.transport.nostr_relay_urls.is_empty()
- {
- let transport = Arc::new(NostrTransport::new(NostrConfig::new(
- sdk_nostr_relay_url_policy(config),
- &config.transport.nostr_relay_urls,
- )?));
- let source: Arc<dyn EventSource> = transport.clone();
- let sink: Arc<dyn EventSink> = transport;
- Ok((source, sink))
- } else {
- let transport = Arc::new(UnavailableTransport);
- let source: Arc<dyn EventSource> = transport.clone();
- let sink: Arc<dyn EventSink> = transport;
- Ok((source, sink))
- }
-}
-
-fn now_unix_ms() -> Result<u64, RuntimeError> {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|error| RuntimeError::Config(format!("system clock error: {error}")))?
- .as_millis()
- .try_into()
- .map_err(|_| RuntimeError::Config("system clock is outside SDK range".to_owned()))
-}
-
-fn new_source_generation() -> Result<SourceGeneration, RuntimeError> {
- let mut bytes = [0_u8; 32];
- getrandom::getrandom(&mut bytes).map_err(|error| {
- RuntimeError::Config(format!("failed to generate SDK source identity: {error}"))
- })?;
- SourceGeneration::new(bytes)
- .map_err(|error| RuntimeError::Config(format!("invalid SDK source identity: {error}")))
-}
-
-struct SystemClock;
-
-impl Clock for SystemClock {
- fn now_unix_ms(&self) -> Result<u64, radroots_sync::Error> {
- now_unix_ms().map_err(|_| radroots_sync::Error::ClockUnavailable)
- }
-}
-
-struct RandomIds;
-
-impl IdSource for RandomIds {
- fn next_id(&self, _operation: OperationKind) -> Result<SyncId, radroots_sync::Error> {
- let mut bytes = [0_u8; 16];
- getrandom::getrandom(&mut bytes).map_err(|_| radroots_sync::Error::InvalidSyncId)?;
- SyncId::new(bytes)
- }
-}
-
-#[derive(Clone)]
-struct SharedProvider(radroots_sdk::signing::Provider);
-
-impl Signer for SharedProvider {
- fn status(&self) -> SigningFuture<'_, Result<SignerStatus, radroots_signing::Error>> {
- Box::pin(self.0.status())
- }
-
- fn sign(
- &self,
- request: SignRequest,
- ) -> SigningFuture<'_, Result<SignReceipt, radroots_signing::Error>> {
- Box::pin(self.0.sign(request))
- }
-}
-
-struct UnavailableTransport;
-
-impl EventSource for UnavailableTransport {
- fn status(&self) -> BoxFuture<'_, Result<SourceStatus, TransportError>> {
- Box::pin(async {
- Ok(SourceStatus::new(
- TransportId::LOCAL,
- false,
- Maturity::Stable,
- Availability::Unavailable,
- SourceCapabilities::NONE,
- LOCAL_TRANSPORT_MESSAGE,
- ))
- })
- }
-
- fn fetch(&self, _request: FetchRequest) -> BoxFuture<'_, Result<FetchPage, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-}
-
-impl EventSink for UnavailableTransport {
- fn status(&self) -> BoxFuture<'_, Result<SinkStatus, TransportError>> {
- Box::pin(async {
- Ok(SinkStatus::new(
- TransportId::LOCAL,
- false,
- Maturity::Stable,
- Availability::Unavailable,
- SinkCapabilities::NONE,
- LOCAL_TRANSPORT_MESSAGE,
- ))
- })
- }
-
- fn deliver(
- &self,
- _request: DeliveryRequest,
- ) -> BoxFuture<'_, Result<DeliveryReceipt, TransportError>> {
- Box::pin(async { Err(TransportError::UnsupportedOperation) })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn random_host_policies_produce_valid_values() {
- assert!(SystemClock.now_unix_ms().expect("clock") > 0);
- assert_ne!(
- RandomIds
- .next_id(OperationKind::Pull)
- .expect("sync id")
- .as_bytes(),
- &[0; 16]
- );
- }
-}
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -1,622 +0,0 @@
-use crate::runtime::RuntimeError;
-use crate::runtime::account::AccountRuntimeFailure;
-use crate::runtime::account::{SHARED_ACCOUNT_STORE_SOURCE, empty_account_resolution_view};
-use crate::runtime::config::{
- CapabilityBindingTargetKind, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend,
-};
-use crate::runtime::sdk::{MYC_NIP46_SESSION_SECRET_SERVICE, myc_managed_account_ref_matches};
-use crate::view::runtime::{
- IdentityPublicView, LocalSignerStatusView, MycStatusView, SignerBindingStatusView,
- SignerStatusView, SignerWriteKindReadinessView,
-};
-use radroots_event::envelope::kind::{
- KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
- KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
-};
-use radroots_identity::account::Status as AccountStatus;
-use url::Url;
-
-const SIGNER_BINDING_PROVIDER_RUNTIME_ID: &str = "myc";
-const SIGNER_BINDING_MODEL: &str = "session_authorized_remote_signer";
-
-#[derive(Debug, Clone, Copy)]
-struct CliWriteKind {
- command: &'static str,
- event_kind: u32,
-}
-
-#[derive(Debug, Clone)]
-pub enum ActorWriteBindingError {
- Unconfigured(String),
- Account(AccountRuntimeFailure),
-}
-
-impl ActorWriteBindingError {
- pub fn from_runtime(error: RuntimeError) -> Self {
- match error {
- RuntimeError::Account(failure) => Self::Account(failure),
- other => Self::Unconfigured(other.to_string()),
- }
- }
-
- pub fn reason(self) -> String {
- match self {
- Self::Unconfigured(reason) => reason,
- Self::Account(failure) => failure.to_string(),
- }
- }
-}
-
-pub fn resolve_signer_status(config: &RuntimeConfig) -> SignerStatusView {
- match config.signer.backend {
- SignerBackend::Local => resolve_local_signer_status(config),
- SignerBackend::Myc => resolve_myc_signer_status(config),
- }
-}
-
-fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
- let (account_resolution, resolved_account_id) =
- match crate::runtime::account::resolve_account_resolution(config) {
- Ok(resolution) => (
- crate::runtime::account::account_resolution_view(&resolution),
- resolution
- .resolved_account
- .as_ref()
- .map(|account| account.record.id().to_string()),
- ),
- Err(error) => {
- let reason = error.to_string();
- return SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "error".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: None,
- account_resolution: empty_account_resolution_view(),
- reason: Some(reason.clone()),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(false, Some(reason)),
- local: None,
- myc: None,
- };
- }
- };
- let secret_backend = crate::runtime::account::secret_backend_status(config);
- if secret_backend.state == "unavailable" {
- let reason = secret_backend.reason.clone();
- return SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "unavailable".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: resolved_account_id.clone(),
- account_resolution: account_resolution.clone(),
- reason: reason.clone(),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(false, reason),
- local: None,
- myc: None,
- };
- }
-
- if secret_backend.state == "error" {
- let reason = secret_backend.reason.clone();
- return SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "error".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: resolved_account_id.clone(),
- account_resolution: account_resolution.clone(),
- reason: reason.clone(),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(false, reason),
- local: None,
- myc: None,
- };
- }
-
- let backend = secret_backend
- .active_backend
- .unwrap_or_else(|| "unknown".to_owned());
-
- match crate::runtime::account::resolved_account_signing_status(config) {
- Ok(AccountStatus::Ready { account }) => SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "ready".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: Some(account.id().to_string()),
- account_resolution: account_resolution.clone(),
- reason: None,
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(true, None),
- local: Some(LocalSignerStatusView {
- account_id: account.id().to_string(),
- public_identity: IdentityPublicView::from_public_identity(
- account.public_identity(),
- ),
- availability: "secret_backed".to_owned(),
- secret_backed: true,
- backend: backend.clone(),
- }),
- myc: None,
- },
- Ok(AccountStatus::PublicOnly { account }) => {
- let reason = AccountRuntimeFailure::watch_only(&account.id()).to_string();
- SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "unconfigured".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: Some(account.id().to_string()),
- account_resolution: account_resolution.clone(),
- reason: Some(reason.clone()),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(false, Some(reason)),
- local: Some(LocalSignerStatusView {
- account_id: account.id().to_string(),
- public_identity: IdentityPublicView::from_public_identity(
- account.public_identity(),
- ),
- availability: "public_only".to_owned(),
- secret_backed: false,
- backend: backend.clone(),
- }),
- myc: None,
- }
- }
- Ok(AccountStatus::NotConfigured) => SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "unconfigured".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: None,
- account_resolution: account_resolution.clone(),
- reason: crate::runtime::account::unresolved_account_reason(config).ok(),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(
- false,
- crate::runtime::account::unresolved_account_reason(config).ok(),
- ),
- local: None,
- myc: None,
- },
- Ok(_) => SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "error".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: resolved_account_id,
- account_resolution,
- reason: Some("account status is not supported by this CLI version".to_owned()),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(
- false,
- Some("account status is not supported by this CLI version".to_owned()),
- ),
- local: None,
- myc: None,
- },
- Err(error) => {
- let reason = error.to_string();
- SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: "error".to_owned(),
- source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- signer_account_id: resolved_account_id,
- account_resolution,
- reason: Some(reason.clone()),
- binding: disabled_binding_status(),
- write_kinds: local_write_kind_readiness(false, Some(reason)),
- local: None,
- myc: None,
- }
- }
- }
-}
-
-fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView {
- let (account_resolution, actor_account_id, actor_pubkey) =
- match crate::runtime::account::resolve_account_resolution(config) {
- Ok(resolution) => {
- let actor_account_id = resolution
- .resolved_account
- .as_ref()
- .map(|account| account.record.id().to_string());
- let actor_pubkey = resolution
- .resolved_account
- .as_ref()
- .map(|account| account.record.public_identity().public_key().to_hex());
- (
- crate::runtime::account::account_resolution_view(&resolution),
- actor_account_id,
- actor_pubkey,
- )
- }
- Err(_) => (empty_account_resolution_view(), None, None),
- };
- let readiness =
- myc_binding_readiness(config, actor_account_id.as_deref(), actor_pubkey.as_deref());
- SignerStatusView {
- mode: config.signer.backend.as_str().to_owned(),
- state: if readiness.ready {
- "ready"
- } else {
- "unconfigured"
- }
- .to_owned(),
- source: readiness.source.clone(),
- signer_account_id: None,
- account_resolution,
- reason: readiness.reason.clone(),
- binding: readiness.binding,
- write_kinds: myc_write_kind_readiness(readiness.ready, readiness.reason.clone()),
- local: None,
- myc: Some(MycStatusView {
- executable: config.myc.executable.display().to_string(),
- state: if readiness.ready {
- "ready"
- } else {
- "unconfigured"
- }
- .to_owned(),
- source: readiness.source,
- service_status: None,
- ready: readiness.ready,
- reason: readiness.reason,
- reasons: readiness.reasons,
- remote_session_count: usize::from(readiness.signer_session_ref.is_some()),
- local_signer: None,
- remote_sessions: Vec::new(),
- custody: None,
- }),
- }
-}
-
-fn disabled_binding_status() -> SignerBindingStatusView {
- SignerBindingStatusView {
- capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(),
- provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(),
- binding_model: SIGNER_BINDING_MODEL.to_owned(),
- state: "disabled".to_owned(),
- source: "independent local signer mode".to_owned(),
- target_kind: None,
- target: None,
- managed_account_ref: None,
- signer_session_ref: None,
- resolved_session_ref: None,
- matched_session_count: None,
- reason: Some(
- "remote myc signer binding is disabled while cli signer mode is `local`".to_owned(),
- ),
- }
-}
-
-fn cli_write_kinds() -> [CliWriteKind; 9] {
- [
- CliWriteKind {
- command: "farm.publish",
- event_kind: KIND_FARM,
- },
- CliWriteKind {
- command: "listing.publish",
- event_kind: KIND_CLASSIFIED_LISTING,
- },
- CliWriteKind {
- command: "listing.update",
- event_kind: KIND_CLASSIFIED_LISTING,
- },
- CliWriteKind {
- command: "listing.withdraw",
- event_kind: KIND_CLASSIFIED_LISTING,
- },
- CliWriteKind {
- command: "trade.proposal.submit",
- event_kind: KIND_TRADE_PROPOSAL,
- },
- CliWriteKind {
- command: "trade.candidate.decide",
- event_kind: KIND_TRADE_DECISION,
- },
- CliWriteKind {
- command: "trade.revision.propose",
- event_kind: KIND_TRADE_REVISION_PROPOSAL,
- },
- CliWriteKind {
- command: "trade.revision.candidate.decide",
- event_kind: KIND_TRADE_REVISION_DECISION,
- },
- CliWriteKind {
- command: "trade.cancellation.submit",
- event_kind: KIND_TRADE_CANCELLATION,
- },
- ]
-}
-
-fn local_write_kind_readiness(
- ready: bool,
- reason: Option<String>,
-) -> Vec<SignerWriteKindReadinessView> {
- cli_write_kinds()
- .iter()
- .map(|kind| SignerWriteKindReadinessView {
- command: kind.command.to_owned(),
- event_kind: kind.event_kind,
- permission: "local_account_secret".to_owned(),
- ready,
- reason: if ready { None } else { reason.clone() },
- })
- .collect()
-}
-
-#[derive(Debug, Clone)]
-struct MycBindingReadiness {
- binding: SignerBindingStatusView,
- ready: bool,
- source: String,
- reason: Option<String>,
- reasons: Vec<String>,
- signer_session_ref: Option<String>,
-}
-
-fn myc_binding_readiness(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: Option<&str>,
-) -> MycBindingReadiness {
- let Some(binding) = config.capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY) else {
- let reason = "signer.remote_nip46 binding is missing".to_owned();
- return MycBindingReadiness {
- binding: missing_myc_binding_status(reason.clone()),
- ready: false,
- source: "no explicit capability binding".to_owned(),
- reason: Some(reason.clone()),
- reasons: vec![reason],
- signer_session_ref: None,
- };
- };
-
- let mut reasons = Vec::new();
- if binding.target_kind != CapabilityBindingTargetKind::ExplicitEndpoint {
- reasons.push(format!(
- "signer.remote_nip46 binding target_kind `{}` is not supported for CLI Myc signing; use `explicit_endpoint`",
- binding.target_kind.as_str()
- ));
- }
- if let Err(reason) = validate_myc_target(binding.target.as_str()) {
- reasons.push(reason);
- }
- if let Some(managed_account_ref) = binding.managed_account_ref.as_deref() {
- let managed_account_matches = actor_pubkey
- .map(|actor_pubkey| {
- myc_managed_account_ref_matches(managed_account_ref, actor_account_id, actor_pubkey)
- })
- .unwrap_or_else(|| {
- actor_account_id.is_some_and(|account_id| managed_account_ref == account_id)
- });
- if !managed_account_matches {
- let reason = if actor_account_id.is_none() && actor_pubkey.is_none() {
- format!(
- "signer.remote_nip46 managed_account_ref `{managed_account_ref}` cannot be evaluated because no actor account or pubkey resolved"
- )
- } else {
- format!(
- "signer.remote_nip46 managed_account_ref `{managed_account_ref}` does not match actor account or pubkey"
- )
- };
- reasons.push(reason);
- }
- }
- let signer_session_ref = binding.signer_session_ref.clone();
- if let Some(session_ref) = signer_session_ref.as_deref() {
- match crate::runtime::account::load_secret_backend_secret(
- config,
- session_ref,
- MYC_NIP46_SESSION_SECRET_SERVICE,
- ) {
- Ok(Some(secret)) if secret.trim().is_empty() => {
- reasons.push(format!(
- "signer.remote_nip46 signer_session_ref `{session_ref}` resolved to an empty client secret"
- ));
- }
- Ok(Some(_)) => {}
- Ok(None) => {
- reasons.push(format!(
- "signer.remote_nip46 signer_session_ref `{session_ref}` was not found in the account secret backend"
- ));
- }
- Err(error) => reasons.push(error.to_string()),
- }
- } else {
- reasons.push("signer.remote_nip46 signer_session_ref is missing".to_owned());
- }
-
- let ready = reasons.is_empty();
- let reason = reasons.first().cloned();
- let source = binding.source.as_str().to_owned();
- MycBindingReadiness {
- binding: SignerBindingStatusView {
- capability_id: binding.capability_id.clone(),
- provider_runtime_id: binding.provider_runtime_id.clone(),
- binding_model: binding.binding_model.clone(),
- state: if ready { "ready" } else { "unconfigured" }.to_owned(),
- source: source.clone(),
- target_kind: Some(binding.target_kind.as_str().to_owned()),
- target: Some(binding.target.clone()),
- managed_account_ref: binding.managed_account_ref.clone(),
- signer_session_ref: binding.signer_session_ref.clone(),
- resolved_session_ref: binding.signer_session_ref.clone().filter(|_| ready),
- matched_session_count: Some(usize::from(ready)),
- reason: reason.clone(),
- },
- ready,
- source,
- reason,
- reasons,
- signer_session_ref,
- }
-}
-
-fn missing_myc_binding_status(reason: String) -> SignerBindingStatusView {
- SignerBindingStatusView {
- capability_id: SIGNER_REMOTE_NIP46_CAPABILITY.to_owned(),
- provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(),
- binding_model: SIGNER_BINDING_MODEL.to_owned(),
- state: "unconfigured".to_owned(),
- source: "no explicit capability binding".to_owned(),
- target_kind: None,
- target: None,
- managed_account_ref: None,
- signer_session_ref: None,
- resolved_session_ref: None,
- matched_session_count: Some(0),
- reason: Some(reason),
- }
-}
-
-fn validate_myc_target(value: &str) -> Result<(), String> {
- let trimmed = value.trim();
- if trimmed.starts_with("nostrconnect://") {
- return Err(
- "signer.remote_nip46 target must be a bunker URI or discovery URL; raw nostrconnect client URIs are signer-side only"
- .to_owned(),
- );
- }
- let bunker_uri = if trimmed.starts_with("bunker://") {
- trimmed.to_owned()
- } else {
- let url = Url::parse(trimmed)
- .map_err(|error| format!("signer.remote_nip46 target is invalid: {error}"))?;
- url.query_pairs()
- .find(|(key, _)| key == "uri")
- .map(|(_, uri)| uri.into_owned())
- .ok_or_else(|| {
- "signer.remote_nip46 discovery target is missing `uri` query parameter".to_owned()
- })?
- };
- match radroots_nostr_connect::uri::Uri::parse(bunker_uri.as_str())
- .map_err(|error| format!("signer.remote_nip46 target is invalid: {error}"))?
- {
- radroots_nostr_connect::uri::Uri::Bunker(_) => Ok(()),
- radroots_nostr_connect::uri::Uri::Client(_) => Err(
- "signer.remote_nip46 target must resolve to a bunker URI; raw nostrconnect client URIs are signer-side only"
- .to_owned(),
- ),
- }
-}
-
-fn myc_write_kind_readiness(
- ready: bool,
- reason: Option<String>,
-) -> Vec<SignerWriteKindReadinessView> {
- cli_write_kinds()
- .iter()
- .map(|kind| {
- let permission = sign_event_permission_for_kind(kind.event_kind);
- SignerWriteKindReadinessView {
- command: kind.command.to_owned(),
- event_kind: kind.event_kind,
- permission,
- ready,
- reason: if ready { None } else { reason.clone() },
- }
- })
- .collect()
-}
-
-fn sign_event_permission_for_kind(event_kind: u32) -> String {
- format!("sign_event:{event_kind}")
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
- KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
- cli_write_kinds, myc_managed_account_ref_matches, myc_write_kind_readiness,
- sign_event_permission_for_kind,
- };
-
- #[test]
- fn write_kind_readiness_matches_active_signed_mutations() {
- let commands: Vec<&str> = cli_write_kinds()
- .iter()
- .map(|write_kind| write_kind.command)
- .collect();
-
- assert_eq!(
- commands,
- [
- "farm.publish",
- "listing.publish",
- "listing.update",
- "listing.withdraw",
- "trade.proposal.submit",
- "trade.candidate.decide",
- "trade.revision.propose",
- "trade.revision.candidate.decide",
- "trade.cancellation.submit",
- ]
- );
- assert!(!commands.contains(&"signer.status"));
- assert!(!commands.contains(&"sync.push"));
- }
-
- #[test]
- fn trade_readiness_uses_canonical_mutation_kinds() {
- for (command, event_kind) in [
- ("trade.proposal.submit", KIND_TRADE_PROPOSAL),
- ("trade.candidate.decide", KIND_TRADE_DECISION),
- ("trade.revision.propose", KIND_TRADE_REVISION_PROPOSAL),
- (
- "trade.revision.candidate.decide",
- KIND_TRADE_REVISION_DECISION,
- ),
- ("trade.cancellation.submit", KIND_TRADE_CANCELLATION),
- ] {
- let write_kind = cli_write_kinds()
- .into_iter()
- .find(|kind| kind.command == command)
- .expect("trade readiness");
-
- assert_eq!(write_kind.event_kind, event_kind);
- }
- }
-
- #[test]
- fn myc_write_readiness_requires_exact_permissions() {
- let readiness = myc_write_kind_readiness(true, None);
- assert!(readiness.iter().all(|kind| kind.command != "sync.push"));
-
- for (command, event_kind) in [
- ("farm.publish", KIND_FARM),
- ("listing.publish", KIND_CLASSIFIED_LISTING),
- ("trade.proposal.submit", KIND_TRADE_PROPOSAL),
- ] {
- let entry = readiness
- .iter()
- .find(|kind| kind.command == command)
- .expect("product write readiness");
-
- assert_eq!(entry.permission, sign_event_permission_for_kind(event_kind));
- assert!(entry.ready, "{command} should be ready");
- assert_eq!(entry.reason, None);
- }
- }
-
- #[test]
- fn myc_managed_account_ref_matches_actor_account_id_or_pubkey() {
- let actor_account_id = Some("acct_farmer_market");
- let actor_pubkey = "02d67b520cb0b835a5ca6ddf78bf3bbfe636d31a523050efc01bf8cb0c680da09e";
-
- assert!(myc_managed_account_ref_matches(
- "acct_farmer_market",
- actor_account_id,
- actor_pubkey,
- ));
- assert!(myc_managed_account_ref_matches(
- actor_pubkey,
- actor_account_id,
- actor_pubkey,
- ));
- assert!(!myc_managed_account_ref_matches(
- "acct_other",
- actor_account_id,
- actor_pubkey,
- ));
- }
-}
diff --git a/src/runtime/signing.rs b/src/runtime/signing.rs
@@ -1,619 +0,0 @@
-//! CLI-owned composition of canonical local and NIP-46 signer adapters.
-
-use std::{
- sync::Arc,
- time::{Duration, SystemTime, UNIX_EPOCH},
-};
-
-use nostr_sdk::prelude::{
- Client as RelayClient, Filter, JsonUtil, Kind, RelayPoolNotification, Timestamp,
-};
-use radroots_event::{SignedEvent, wire::Nip01EventWire};
-use radroots_nostr_connect::{
- Client as NostrConnectClient, Error as NostrConnectError, Request, Response,
- client::{
- CancellationToken, ClientEvent, Completion, Progress, Receive, Target, Transport,
- TransportFuture,
- },
- message::{RequestId, UnsignedEvent},
- uri::{BunkerUri, Uri},
-};
-use radroots_sdk::signing::Provider;
-use radroots_signing::{
- Error as SigningError, SignReceipt, SignRequest, Signer, SignerStatus,
- capability::{CancellationSupport, SignerCapability, SignerKind},
- error::Kind as SigningErrorKind,
- signer::BoxFuture,
- status::{AuthChallenge, SignProgress, SignProgressStage, SignerAvailability},
-};
-use tokio::{sync::broadcast, time::Instant};
-use url::Url;
-use zeroize::Zeroizing;
-
-use crate::runtime::{
- RuntimeError, account,
- config::{
- CapabilityBindingTargetKind, RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend,
- },
-};
-
-pub(crate) const MYC_NIP46_SESSION_SECRET_SERVICE: &str = "org.radroots.cli.myc-nip46-session";
-
-pub(crate) fn validate_for_actor(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
- actor_label: &str,
-) -> Result<(), RuntimeError> {
- match config.signer.backend {
- SignerBackend::Local => {
- let signing = local_signer(config, actor_account_id)?;
- let signer_pubkey = signing.signer.public_key().to_hex();
- if !signer_pubkey.eq_ignore_ascii_case(actor_pubkey) {
- return Err(account::AccountRuntimeFailure::mismatch(format!(
- "{actor_label} public key `{actor_pubkey}` does not match local signer account `{}` public key `{signer_pubkey}`",
- signing.account.record.id()
- ))
- .into());
- }
- Ok(())
- }
- SignerBackend::Myc => remote_input(config, actor_account_id, actor_pubkey).map(|_| ()),
- }
-}
-
-pub(crate) async fn provider_for_actor(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
- actor_label: &str,
-) -> Result<Provider, RuntimeError> {
- match config.signer.backend {
- SignerBackend::Local => {
- let signing = local_signer(config, actor_account_id)?;
- let signer_pubkey = signing.signer.public_key().to_hex();
- if !signer_pubkey.eq_ignore_ascii_case(actor_pubkey) {
- return Err(account::AccountRuntimeFailure::mismatch(format!(
- "{actor_label} public key `{actor_pubkey}` does not match local signer account `{}` public key `{signer_pubkey}`",
- signing.account.record.id()
- ))
- .into());
- }
- Ok(Provider::local(signing.signer))
- }
- SignerBackend::Myc => {
- let input = remote_input(config, actor_account_id, actor_pubkey)?;
- let signer = Nip46Signer::connect(input).await?;
- Ok(Provider::nip46(Arc::new(signer)))
- }
- }
-}
-
-fn local_signer(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
-) -> Result<account::AccountLocalSigner, RuntimeError> {
- match actor_account_id {
- Some(account_id) => account::resolve_local_signing_identity_for_account(config, account_id),
- None => account::resolve_local_signing_identity(config),
- }
-}
-
-struct RemoteInput {
- session_secret: Zeroizing<String>,
- bunker: BunkerUri,
- request_timeout: Duration,
-}
-
-fn remote_input(
- config: &RuntimeConfig,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
-) -> Result<RemoteInput, RuntimeError> {
- let binding = config
- .capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY)
- .ok_or_else(|| RuntimeError::Config("signer.remote_nip46 binding is missing".to_owned()))?;
- if binding.target_kind != CapabilityBindingTargetKind::ExplicitEndpoint {
- return Err(RuntimeError::Config(format!(
- "signer.remote_nip46 binding target_kind `{}` is not supported for CLI Myc signing; use `explicit_endpoint`",
- binding.target_kind.as_str()
- )));
- }
- if let Some(managed_account_ref) = binding.managed_account_ref.as_deref()
- && !myc_managed_account_ref_matches(managed_account_ref, actor_account_id, actor_pubkey)
- {
- return Err(RuntimeError::Config(format!(
- "signer.remote_nip46 managed_account_ref `{managed_account_ref}` does not match actor account or pubkey"
- )));
- }
- let signer_session_ref = binding.signer_session_ref.as_deref().ok_or_else(|| {
- RuntimeError::Config("signer.remote_nip46 signer_session_ref is missing".to_owned())
- })?;
- let secret = account::load_secret_backend_secret(
- config,
- signer_session_ref,
- MYC_NIP46_SESSION_SECRET_SERVICE,
- )?
- .ok_or_else(|| {
- RuntimeError::Config(format!(
- "signer.remote_nip46 signer_session_ref `{signer_session_ref}` was not found in the account secret backend"
- ))
- })?;
- if config.myc.status_timeout_ms == 0 {
- return Err(RuntimeError::Config(
- "RADROOTS_CLI_MYC_STATUS_TIMEOUT_MS must be greater than zero".to_owned(),
- ));
- }
- let bunker = parse_target(binding.target.as_str())?;
- if bunker.remote_signer_public_key().to_hex() != actor_pubkey {
- return Err(account::AccountRuntimeFailure::mismatch(format!(
- "remote signer public key `{}` does not match actor public key `{actor_pubkey}`",
- bunker.remote_signer_public_key().to_hex()
- ))
- .into());
- }
- Ok(RemoteInput {
- session_secret: Zeroizing::new(secret),
- bunker,
- request_timeout: Duration::from_millis(config.myc.status_timeout_ms),
- })
-}
-
-pub(crate) fn myc_managed_account_ref_matches(
- managed_account_ref: &str,
- actor_account_id: Option<&str>,
- actor_pubkey: &str,
-) -> bool {
- actor_account_id.is_some_and(|account_id| managed_account_ref == account_id)
- || managed_account_ref == actor_pubkey
-}
-
-fn parse_target(value: &str) -> Result<BunkerUri, RuntimeError> {
- let trimmed = value.trim();
- if trimmed.starts_with("nostrconnect://") {
- return Err(RuntimeError::Config(
- "signer.remote_nip46 target must be a bunker URI or discovery URL; raw nostrconnect client URIs are signer-side only"
- .to_owned(),
- ));
- }
- let bunker_uri = if trimmed.starts_with("bunker://") {
- trimmed.to_owned()
- } else {
- Url::parse(trimmed)
- .map_err(|error| {
- RuntimeError::Config(format!("signer.remote_nip46 target is invalid: {error}"))
- })?
- .query_pairs()
- .find(|(key, _)| key == "uri")
- .map(|(_, uri)| uri.into_owned())
- .ok_or_else(|| {
- RuntimeError::Config(
- "signer.remote_nip46 discovery target is missing `uri` query parameter"
- .to_owned(),
- )
- })?
- };
- match Uri::parse(bunker_uri.as_str())
- .map_err(|error| RuntimeError::Config(format!("signer.remote_nip46 target is invalid: {error}")))?
- {
- Uri::Bunker(bunker) => Ok(bunker),
- Uri::Client(_) => Err(RuntimeError::Config(
- "signer.remote_nip46 target must resolve to a bunker URI; raw nostrconnect client URIs are signer-side only"
- .to_owned(),
- )),
- }
-}
-
-struct Nip46Signer {
- client: NostrConnectClient,
- target: Target,
- request_timeout: Duration,
-}
-
-impl Nip46Signer {
- async fn connect(input: RemoteInput) -> Result<Self, RuntimeError> {
- let target = Target::try_new(
- input.bunker.remote_signer_public_key(),
- input.bunker.relays().to_vec(),
- )
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
- let client = NostrConnectClient::from_secret(input.session_secret.trim(), target.clone())
- .map_err(|_| {
- RuntimeError::Config("NIP-46 client session secret is invalid".to_owned())
- })?;
- Ok(Self {
- client,
- target,
- request_timeout: input.request_timeout,
- })
- }
-}
-
-impl Signer for Nip46Signer {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> {
- Box::pin(async {
- Ok(SignerStatus::new(
- SignerAvailability::Ready,
- vec![SignerCapability::new(
- SignerKind::Remote,
- CancellationSupport::BeforeAndAfterPublication,
- true,
- true,
- )],
- None,
- ))
- })
- }
-
- fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> {
- Box::pin(async move {
- let now =
- unix_time().map_err(|_| SigningError::new(SigningErrorKind::InternalError))?;
- let remaining = request.policy().deadline_unix().saturating_sub(now);
- if remaining == 0 {
- return Err(SigningError::new(SigningErrorKind::DeadlineExceeded));
- }
- request.report_progress(&SignProgress::stage(SignProgressStage::Validating)?);
- let unsigned_json = serde_json::json!({
- "pubkey": request.draft().expected_pubkey().to_hex(),
- "created_at": request.draft().created_at_u64(),
- "kind": request.draft().kind_u32(),
- "tags": request.draft().tags_as_vec(),
- "content": request.draft().content(),
- })
- .to_string();
- let unsigned = UnsignedEvent::from_json(unsigned_json.as_str())
- .map_err(|error| signing_source(SigningErrorKind::InvalidArgument, error))?;
- let mut transport = RelayTransport::connect(
- &self.client,
- &self.target,
- self.request_timeout.min(Duration::from_secs(remaining)),
- )
- .await
- .map_err(|error| signing_source(SigningErrorKind::SignerUnavailable, error))?;
- let request_id = random_request_id()
- .map_err(|error| signing_source(SigningErrorKind::InternalError, error))?;
- request.report_progress(&SignProgress::stage(SignProgressStage::RequestPublished)?);
- let cancellation = CancellationToken::new();
- let completion = self
- .client
- .execute(
- request_id,
- Request::SignEvent(unsigned),
- &mut transport,
- &cancellation,
- |progress| report_remote_progress(&request, progress),
- )
- .await
- .map_err(normalize_nip46_error)?;
- finish_remote_signing(&request, completion)
- })
- }
-}
-
-fn report_remote_progress(
- request: &SignRequest,
- progress: Progress,
-) -> Result<(), NostrConnectError> {
- match progress {
- Progress::AuthChallenge { url } => {
- let now = unix_time().map_err(|error| NostrConnectError::Transport {
- reason: error.to_string(),
- })?;
- let challenge = AuthChallenge::new(url, now, Some(request.policy().deadline_unix()))
- .map_err(|_| NostrConnectError::InvalidClientState {
- reason: "remote authentication challenge is invalid",
- })?;
- request.report_progress(&SignProgress::authentication(challenge));
- }
- }
- Ok(())
-}
-
-fn finish_remote_signing(
- request: &SignRequest,
- completion: Completion,
-) -> Result<SignReceipt, SigningError> {
- let Completion::Response(response) = completion else {
- return Err(SigningError::new(SigningErrorKind::SignerCancelled));
- };
- let Response::SignedEvent(event) = *response else {
- return Err(SigningError::new(SigningErrorKind::SignerOutputInvalid));
- };
- request.report_progress(&SignProgress::stage(SignProgressStage::VerifyingOutput)?);
- let raw_json = event.as_json();
- let wire = Nip01EventWire::parse_json(raw_json.as_str())
- .map_err(|error| signing_source(SigningErrorKind::SignerOutputInvalid, error))?;
- let signed = SignedEvent::from_wire_verified_id(wire, raw_json)
- .map_err(|error| signing_source(SigningErrorKind::SignerOutputInvalid, error))?;
- let completed_at =
- unix_time().map_err(|error| signing_source(SigningErrorKind::InternalError, error))?;
- let receipt = SignReceipt::from_signed_event(request, signed, completed_at)?;
- request.report_progress(&SignProgress::stage(SignProgressStage::Complete)?);
- Ok(receipt)
-}
-
-fn normalize_nip46_error(error: NostrConnectError) -> SigningError {
- let kind = match error {
- NostrConnectError::RequestTimedOut => SigningErrorKind::SignerTimeout,
- NostrConnectError::WrongRequestId
- | NostrConnectError::WrongResponseSigner
- | NostrConnectError::ReplayedResponse
- | NostrConnectError::InvalidResponseEnvelope { .. }
- | NostrConnectError::InvalidResponsePayload { .. }
- | NostrConnectError::InvalidClientEvent => SigningErrorKind::SignerOutputInvalid,
- NostrConnectError::Transport { .. } => SigningErrorKind::SignerUnavailable,
- _ => SigningErrorKind::InternalError,
- };
- signing_source(kind, error)
-}
-
-fn signing_source<E>(kind: SigningErrorKind, source: E) -> SigningError
-where
- E: std::error::Error + Send + Sync + 'static,
-{
- SigningError::with_source(kind, source)
-}
-
-fn random_request_id() -> Result<RequestId, getrandom::Error> {
- let mut bytes = [0_u8; 16];
- getrandom::getrandom(&mut bytes)?;
- let value = bytes
- .iter()
- .map(|byte| format!("{byte:02x}"))
- .collect::<String>();
- Ok(RequestId::parse(value).expect("hex request ID is bounded and valid"))
-}
-
-fn unix_time() -> Result<u64, std::time::SystemTimeError> {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|value| value.as_secs())
-}
-
-struct RelayTransport {
- client: RelayClient,
- notifications: broadcast::Receiver<RelayPoolNotification>,
- request_timeout: Duration,
- deadline: Option<Instant>,
-}
-
-impl RelayTransport {
- async fn connect(
- protocol_client: &NostrConnectClient,
- target: &Target,
- request_timeout: Duration,
- ) -> Result<Self, NostrConnectError> {
- let client = RelayClient::default();
- client.automatic_authentication(false);
- for relay in target.relays() {
- client
- .add_relay(relay.to_string())
- .await
- .map_err(transport_error)?;
- }
- let connected = client.try_connect(request_timeout).await;
- if connected.success.is_empty() {
- return Err(NostrConnectError::Transport {
- reason: "no NIP-46 relay accepted the connection".to_owned(),
- });
- }
- let client_public_key = protocol_client.public_key().map_err(|error| error)?;
- let client_public_key = nostr_sdk::prelude::PublicKey::from_slice(
- client_public_key.as_bytes(),
- )
- .map_err(|_| NostrConnectError::InvalidClientState {
- reason: "client public key is invalid",
- })?;
- let filter = Filter::new()
- .kind(Kind::Custom(radroots_nostr_connect::message::RPC_KIND))
- .pubkey(client_public_key)
- .since(Timestamp::now());
- let notifications = client.notifications();
- let subscribed = client
- .subscribe(filter, None)
- .await
- .map_err(transport_error)?;
- if subscribed.success.is_empty() {
- return Err(NostrConnectError::Transport {
- reason: "no NIP-46 relay accepted the response subscription".to_owned(),
- });
- }
- Ok(Self {
- client,
- notifications,
- request_timeout,
- deadline: None,
- })
- }
-}
-
-impl Transport for RelayTransport {
- fn publish<'a>(&'a mut self, event: ClientEvent) -> TransportFuture<'a, ()> {
- Box::pin(async move {
- self.deadline = Some(Instant::now() + self.request_timeout);
- let event = nostr_sdk::prelude::Event::from_json(event.as_json())
- .map_err(|_| NostrConnectError::InvalidClientEvent)?;
- let output = self
- .client
- .send_event(&event)
- .await
- .map_err(transport_error)?;
- if output.success.is_empty() {
- return Err(NostrConnectError::Transport {
- reason: "no NIP-46 relay accepted the request".to_owned(),
- });
- }
- Ok(())
- })
- }
-
- fn receive<'a>(
- &'a mut self,
- cancellation: &'a CancellationToken,
- ) -> TransportFuture<'a, Receive> {
- Box::pin(async move {
- loop {
- if cancellation.is_cancelled() {
- return Ok(Receive::Cancelled);
- }
- let Some(deadline) = self.deadline else {
- return Err(NostrConnectError::InvalidClientState {
- reason: "request deadline is not initialized",
- });
- };
- let now = Instant::now();
- if now >= deadline {
- return Ok(Receive::TimedOut);
- }
- let poll = (deadline - now).min(Duration::from_millis(50));
- match tokio::time::timeout(poll, self.notifications.recv()).await {
- Err(_) => continue,
- Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
- Ok(Err(broadcast::error::RecvError::Closed)) => {
- return Err(NostrConnectError::Transport {
- reason: "NIP-46 relay notification stream closed".to_owned(),
- });
- }
- Ok(Ok(RelayPoolNotification::Event { event, .. })) => {
- return ClientEvent::from_json(event.as_json().as_str())
- .map(Receive::event);
- }
- Ok(Ok(_)) => continue,
- }
- }
- })
- }
-}
-
-fn transport_error(error: impl std::fmt::Display) -> NostrConnectError {
- NostrConnectError::Transport {
- reason: error.to_string(),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::sync::{Arc, Mutex};
-
- use radroots_event::{EventDraft, contract::AuthorRole, envelope::kind::KIND_GEOCHAT};
- use radroots_protocol::runtime::v1::OperationId;
- use radroots_signing::{
- Actor,
- actor::ActorSource,
- request::{CancellationPolicy, ProgressObserver, SignPolicy},
- };
-
- struct RecordingObserver(Mutex<Vec<SignProgressStage>>);
-
- impl ProgressObserver for RecordingObserver {
- fn on_progress(&self, progress: &SignProgress) {
- self.0
- .lock()
- .expect("progress lock")
- .push(progress.stage_value());
- }
- }
-
- fn local_request() -> (radroots_nostr::signing::LocalSigner, SignRequest) {
- let signer = radroots_nostr::signing::LocalSigner::generate().expect("local signer");
- let public_key = signer.public_key();
- let actor = Actor::new(
- public_key,
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Any],
- )
- .expect("actor");
- let now = unix_time().expect("time");
- let draft = EventDraft::new(
- "radroots.social.geochat.v1",
- KIND_GEOCHAT,
- now,
- Vec::new(),
- "CLI NIP-46 fixture",
- public_key.to_hex(),
- )
- .expect("draft");
- let request = SignRequest::new(
- OperationId::SyncPush,
- actor,
- draft,
- SignPolicy::new(now + 120, CancellationPolicy::PreservePublishedRequest)
- .expect("policy"),
- )
- .expect("request");
- (signer, request)
- }
-
- #[tokio::test]
- async fn remote_happy_response_becomes_an_exact_verified_receipt() {
- let (local, request) = local_request();
- let signed = local.sign(request.clone()).await.expect("local fixture");
- let wire = radroots_nostr_connect::message::SignedEvent::from_json(
- signed.signed_event().raw_json(),
- )
- .expect("NIP-46 signed event");
-
- let receipt =
- finish_remote_signing(&request, Completion::response(Response::SignedEvent(wire)))
- .expect("remote receipt");
-
- assert_eq!(receipt.operation_id(), OperationId::SyncPush);
- assert_eq!(
- receipt.signed_event().id_str(),
- request.draft().expected_event_id_hex()
- );
- }
-
- #[test]
- fn remote_auth_challenge_is_presented_through_the_canonical_observer() {
- let (_, request) = local_request();
- let observer = Arc::new(RecordingObserver(Mutex::new(Vec::new())));
- let request = request.with_progress_observer(observer.clone());
-
- report_remote_progress(
- &request,
- Progress::AuthChallenge {
- url: "https://signer.example/approve".to_owned(),
- },
- )
- .expect("auth progress");
-
- assert_eq!(
- observer.0.lock().expect("progress lock").as_slice(),
- &[SignProgressStage::AwaitingAuthentication]
- );
- }
-
- #[test]
- fn timeout_and_wrong_response_errors_map_to_stable_signing_kinds() {
- assert_eq!(
- normalize_nip46_error(NostrConnectError::RequestTimedOut).kind(),
- SigningErrorKind::SignerTimeout
- );
- assert_eq!(
- normalize_nip46_error(NostrConnectError::WrongResponseSigner).kind(),
- SigningErrorKind::SignerOutputInvalid
- );
- }
-
- #[test]
- fn exact_account_or_pubkey_binding_is_required() {
- assert!(myc_managed_account_ref_matches(
- "account-a",
- Some("account-a"),
- "aa"
- ));
- assert!(myc_managed_account_ref_matches(
- "aa",
- Some("account-a"),
- "aa"
- ));
- assert!(!myc_managed_account_ref_matches(
- "account-b",
- Some("account-a"),
- "aa"
- ));
- }
-}
diff --git a/src/runtime/store.rs b/src/runtime/store.rs
@@ -1,640 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_replica_store::export::{ReplicaStoreExportManifestRs, export_manifest};
-use radroots_replica_store::migrations;
-use radroots_replica_sync::radroots_replica_sync_status;
-use radroots_sdk::storage::{IntegrityStatus, Status as StorageStatus};
-use radroots_sql_core::SqlxSqliteExecutor;
-use radroots_storage::{
- backup::{BackupFormatVersion, BackupId, BackupPlan, BackupSecretPolicy, RestorePlan},
- status::{IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy},
-};
-use serde::Serialize;
-use serde_json::{Value, json};
-
-use crate::cli::global::LocalExportFormatArg;
-use crate::runtime::RuntimeError;
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_storage_root};
-use crate::runtime::sync::ensure_sync_run_table;
-use crate::view::runtime::{
- LocalBackupView, LocalDerivedProjectionStatusView, LocalExportView, LocalInitView,
- LocalReplicaCountsView, LocalReplicaSyncView, LocalRestoreView, LocalStatusView,
- SdkIntegrityView, SdkStorageStatusView,
-};
-
-const DERIVED_PROJECTION_SOURCE: &str = "local derived projection cache";
-const SDK_CANONICAL_SOURCE: &str = "SDK canonical event store and outbox";
-const SDK_CANONICAL_STORE: &str = "sdk";
-const SDK_BACKUP_KIND: &str = "sdk_canonical";
-const SDK_BACKUP_MANIFEST_FILE: &str = "manifest.json";
-const SDK_RUNTIME_FILE: &str = "runtime.sqlite";
-const SDK_PRIVATE_FILE: &str = "private.sqlite";
-
-pub fn init(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> {
- let existed = config.local.replica_store_path.exists();
- ensure_local_roots(config)?;
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- migrations::run_all_up(&executor)?;
- ensure_sync_run_table(&executor)?;
- let manifest = export_manifest(&executor)?;
-
- Ok(LocalInitView {
- state: if existed {
- "ready".to_owned()
- } else {
- "initialized".to_owned()
- },
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- replica_store: "ready".to_owned(),
- path: config.local.replica_store_path.display().to_string(),
- replica_store_version: manifest.replica_store_version,
- backup_format_version: manifest.backup_format_version,
- })
-}
-
-pub fn init_preflight(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> {
- validate_local_roots(config)?;
- if config.local.replica_store_path.exists() {
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- ensure_sync_run_table(&executor)?;
- let manifest = export_manifest(&executor)?;
- return Ok(LocalInitView {
- state: "ready".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- replica_store: "ready".to_owned(),
- path: config.local.replica_store_path.display().to_string(),
- replica_store_version: manifest.replica_store_version,
- backup_format_version: manifest.backup_format_version,
- });
- }
-
- Ok(LocalInitView {
- state: "dry_run".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- replica_store: "missing".to_owned(),
- path: config.local.replica_store_path.display().to_string(),
- replica_store_version: String::new(),
- backup_format_version: String::new(),
- })
-}
-
-pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterError> {
- let sdk_root = sdk_storage_root(config);
- let sdk_existed_before_open = sdk_storage_files_exist(sdk_root.as_path());
- let derived_projection = derived_projection_status(config)?;
- let session = CliSdkSession::connect(config)?;
- let receipt = session.block_on(session.sdk().storage_status())?;
- let integrity = session.block_on(session.sdk().storage_integrity())?;
- Ok(sdk_status_view(
- config,
- sdk_root,
- sdk_existed_before_open,
- receipt,
- integrity,
- derived_projection,
- ))
-}
-
-fn derived_projection_status(
- config: &RuntimeConfig,
-) -> Result<LocalDerivedProjectionStatusView, RuntimeError> {
- if !config.local.replica_store_path.exists() {
- return Ok(LocalDerivedProjectionStatusView {
- state: "unconfigured".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- replica_store: "missing".to_owned(),
- path: config.local.replica_store_path.display().to_string(),
- replica_store_version: String::new(),
- backup_format_version: String::new(),
- schema_hash: String::new(),
- counts: LocalReplicaCountsView {
- farms: 0,
- listings: 0,
- profiles: 0,
- relays: 0,
- event_states: 0,
- },
- sync: LocalReplicaSyncView {
- expected_count: 0,
- pending_count: 0,
- },
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store inspect".to_owned()],
- });
- }
-
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- ensure_sync_run_table(&executor)?;
- let manifest = export_manifest(&executor)?;
- let sync = radroots_replica_sync_status(&executor)?;
-
- Ok(LocalDerivedProjectionStatusView {
- state: "ready".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- replica_store: "ready".to_owned(),
- path: config.local.replica_store_path.display().to_string(),
- replica_store_version: manifest.replica_store_version.clone(),
- backup_format_version: manifest.backup_format_version.clone(),
- schema_hash: manifest.schema_hash.clone(),
- counts: manifest_counts(&manifest),
- sync: LocalReplicaSyncView {
- expected_count: sync.expected_count,
- pending_count: sync.pending_count,
- },
- reason: None,
- actions: Vec::new(),
- })
-}
-
-pub fn backup(
- config: &RuntimeConfig,
- output: &Path,
-) -> Result<LocalBackupView, CliSdkAdapterError> {
- ensure_safe_sdk_backup_destination(config, output)?;
- let session = CliSdkSession::connect(config)?;
- let plan = sdk_backup_plan()?;
- let operation = session
- .block_on(session.sdk().storage_operations()?.begin_backup(plan))
- .map_err(|error| RuntimeError::Config(format!("SDK backup planning failed: {error}")))?;
- Ok(LocalBackupView {
- state: format!("{:?}", operation.stage()).to_lowercase(),
- source: SDK_CANONICAL_SOURCE.to_owned(),
- backup_kind: SDK_BACKUP_KIND.to_owned(),
- canonical_store: SDK_CANONICAL_STORE.to_owned(),
- destination: output.display().to_string(),
- file: output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string(),
- event_store_file: None,
- outbox_file: None,
- manifest_file: None,
- size_bytes: 0,
- manifest: json!({
- "backup_id": hex_bytes(operation.plan().backup_id().as_bytes()),
- "format_version": operation.plan().format_version().get(),
- "secret_policy": format!("{:?}", operation.plan().secret_policy()).to_lowercase(),
- "requested_at_unix_ms": operation.plan().requested_at_unix_ms(),
- "revision": operation.revision().get(),
- }),
- reason: Some(
- "backup is durably planned; the host capture worker must complete staged capture, verification, and finalization"
- .to_owned(),
- ),
- actions: vec!["radroots store status".to_owned()],
- })
-}
-
-pub fn backup_preflight(
- config: &RuntimeConfig,
- output: &Path,
-) -> Result<LocalBackupView, CliSdkAdapterError> {
- ensure_safe_sdk_backup_destination(config, output)?;
- let session = CliSdkSession::connect(config)?;
- let status = session.block_on(session.sdk().storage_status())?;
- let integrity = session.block_on(session.sdk().storage_integrity())?;
- let manifest = sdk_backup_manifest_preview(output, &status, &integrity);
- Ok(LocalBackupView {
- state: "dry_run".to_owned(),
- source: SDK_CANONICAL_SOURCE.to_owned(),
- backup_kind: SDK_BACKUP_KIND.to_owned(),
- canonical_store: SDK_CANONICAL_STORE.to_owned(),
- destination: output.display().to_string(),
- file: output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string(),
- event_store_file: Some(output.join(SDK_RUNTIME_FILE).display().to_string()),
- outbox_file: Some(output.join(SDK_RUNTIME_FILE).display().to_string()),
- manifest_file: Some(output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string()),
- size_bytes: 0,
- manifest,
- reason: Some(
- "dry run requested; SDK canonical backup directory was not written".to_owned(),
- ),
- actions: vec!["radroots store backup".to_owned()],
- })
-}
-
-pub fn restore(
- config: &RuntimeConfig,
- source: &Path,
- destination: Option<&Path>,
- overwrite: bool,
- dry_run: bool,
-) -> Result<LocalRestoreView, CliSdkAdapterError> {
- let destination = destination
- .map(Path::to_path_buf)
- .unwrap_or_else(|| sdk_storage_root(config));
- ensure_safe_sdk_restore_destination(config, &destination)?;
- let manifest_path = source.join(SDK_BACKUP_MANIFEST_FILE);
- let manifest = serde_json::from_slice::<radroots_storage::backup::BackupManifest>(&fs::read(
- &manifest_path,
- )?)
- .map_err(|error| RuntimeError::Config(format!("invalid backup manifest: {error}")))?;
- let plan = RestorePlan::new(manifest.clone(), manifest.secret_policy(), unix_ms()?)
- .map_err(|error| RuntimeError::Config(format!("invalid restore plan: {error}")))?;
- let session = CliSdkSession::connect(config)?;
- let operation = session
- .block_on(session.sdk().storage_operations()?.begin_restore(plan))
- .map_err(|error| RuntimeError::Config(format!("SDK restore planning failed: {error}")))?;
- if !dry_run {
- return Err(RuntimeError::Config(
- "restore is planned but requires the host staging and atomic replacement worker"
- .to_owned(),
- )
- .into());
- }
- Ok(LocalRestoreView {
- state: "dry_run".to_owned(),
- source: SDK_CANONICAL_SOURCE.to_owned(),
- restore_kind: SDK_BACKUP_KIND.to_owned(),
- canonical_store: SDK_CANONICAL_STORE.to_owned(),
- backup_source: source.display().to_string(),
- destination: destination.display().to_string(),
- event_store_file: source.join(SDK_RUNTIME_FILE).display().to_string(),
- outbox_file: source.join(SDK_RUNTIME_FILE).display().to_string(),
- manifest_file: manifest_path.display().to_string(),
- destination_event_store_file: Some(
- destination.join(SDK_RUNTIME_FILE).display().to_string(),
- ),
- destination_outbox_file: Some(destination.join(SDK_RUNTIME_FILE).display().to_string()),
- restored_event_store_file: None,
- restored_outbox_file: None,
- manifest: json_value(&manifest)?,
- verification: json!({
- "stage": format!("{:?}", operation.stage()).to_lowercase(),
- "revision": operation.revision().get(),
- }),
- overwrite,
- dry_run,
- reason: Some("dry run requested; restore was validated and not staged".to_owned()),
- actions: vec!["radroots store restore <backup-dir>".to_owned()],
- })
-}
-
-pub fn export(
- config: &RuntimeConfig,
- format: LocalExportFormatArg,
- output: &Path,
-) -> Result<LocalExportView, RuntimeError> {
- if !config.local.replica_store_path.exists() {
- return Ok(LocalExportView {
- state: "unconfigured".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- format: format.as_str().to_owned(),
- file: output.display().to_string(),
- records: 0,
- export_version: String::new(),
- schema_hash: String::new(),
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store inspect".to_owned()],
- });
- }
-
- ensure_safe_output_path(config, output)?;
- create_parent_dir(output)?;
-
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- let manifest = export_manifest(&executor)?;
- let sync = radroots_replica_sync_status(&executor)?;
- let records = match format {
- LocalExportFormatArg::Json => {
- let export = json!({
- "kind": "local_export_manifest_v1",
- "source": DERIVED_PROJECTION_SOURCE,
- "replica_store_version": manifest.replica_store_version,
- "backup_format_version": manifest.backup_format_version,
- "export_version": manifest.export_version,
- "schema_hash": manifest.schema_hash,
- "sync": {
- "expected_count": sync.expected_count,
- "pending_count": sync.pending_count,
- },
- "table_counts": manifest.table_counts,
- });
- fs::write(output, serde_json::to_string_pretty(&export)?)?;
- 1
- }
- LocalExportFormatArg::Ndjson => {
- let mut lines = Vec::new();
- lines.push(
- json!({
- "kind": "local_export_manifest",
- "source": DERIVED_PROJECTION_SOURCE,
- "replica_store_version": manifest.replica_store_version,
- "backup_format_version": manifest.backup_format_version,
- "export_version": manifest.export_version,
- "schema_hash": manifest.schema_hash,
- })
- .to_string(),
- );
- lines.push(
- json!({
- "kind": "local_sync_status",
- "expected_count": sync.expected_count,
- "pending_count": sync.pending_count,
- })
- .to_string(),
- );
- for table in &manifest.table_counts {
- lines.push(
- json!({
- "kind": "local_table_count",
- "table": table.name,
- "row_count": table.row_count,
- })
- .to_string(),
- );
- }
- fs::write(output, format!("{}\n", lines.join("\n")))?;
- lines.len()
- }
- };
-
- Ok(LocalExportView {
- state: "exported".to_owned(),
- source: DERIVED_PROJECTION_SOURCE.to_owned(),
- format: format.as_str().to_owned(),
- file: output.display().to_string(),
- records,
- export_version: manifest.export_version,
- schema_hash: manifest.schema_hash,
- reason: None,
- actions: Vec::new(),
- })
-}
-
-fn ensure_local_roots(config: &RuntimeConfig) -> Result<(), RuntimeError> {
- fs::create_dir_all(&config.local.root)?;
- fs::create_dir_all(&config.local.backups_dir)?;
- fs::create_dir_all(&config.local.exports_dir)?;
- Ok(())
-}
-
-fn validate_local_roots(config: &RuntimeConfig) -> Result<(), RuntimeError> {
- validate_directory_target(&config.local.root)?;
- validate_directory_target(&config.local.backups_dir)?;
- validate_directory_target(&config.local.exports_dir)?;
- Ok(())
-}
-
-fn validate_directory_target(path: &Path) -> Result<(), RuntimeError> {
- let mut candidate = path.to_path_buf();
- loop {
- if candidate.exists() {
- if candidate.is_dir() {
- return Ok(());
- }
- return Err(RuntimeError::Config(format!(
- "path {} is not a directory",
- candidate.display()
- )));
- }
- if !candidate.pop() {
- return Err(RuntimeError::Config(format!(
- "path {} has no existing parent directory",
- path.display()
- )));
- }
- }
-}
-
-fn sdk_storage_files_exist(sdk_root: &Path) -> bool {
- sdk_root.join(SDK_RUNTIME_FILE).exists() && sdk_root.join(SDK_PRIVATE_FILE).exists()
-}
-
-fn sdk_status_view(
- config: &RuntimeConfig,
- sdk_root: PathBuf,
- sdk_existed_before_open: bool,
- status: StorageStatus,
- integrity: IntegrityStatus,
- derived_projection: LocalDerivedProjectionStatusView,
-) -> LocalStatusView {
- let state = if integrity.health() == IntegrityHealth::Healthy {
- "ready"
- } else {
- "needs_attention"
- };
- LocalStatusView {
- state: state.to_owned(),
- source: SDK_CANONICAL_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- canonical_store: SDK_CANONICAL_STORE.to_owned(),
- sdk_storage: storage_backend_label(status.backend()).to_owned(),
- sdk_root: sdk_root.display().to_string(),
- sdk_existed_before_open,
- storage: SdkStorageStatusView {
- backend: storage_backend_label(status.backend()).to_owned(),
- open_mode: storage_open_mode_label(status.open_mode()).to_owned(),
- writer_policy: writer_policy_label(status.writer_policy()).to_owned(),
- shutdown: shutdown_state_label(status.shutdown()).to_owned(),
- wal_enabled: status.wal_enabled(),
- busy_timeout_ms: status.busy_timeout_ms(),
- },
- integrity: SdkIntegrityView {
- health: integrity_health_label(integrity.health()).to_owned(),
- checked_at_unix_ms: integrity.checked_at_unix_ms(),
- verified_members: integrity.verified_members(),
- failed_members: integrity.failed_members(),
- },
- derived_projection,
- reason: (state != "ready")
- .then(|| "SDK canonical store integrity requires attention".to_owned()),
- actions: if state == "ready" {
- Vec::new()
- } else {
- vec!["radroots store inspect".to_owned()]
- },
- }
-}
-
-fn storage_backend_label(value: StorageBackend) -> &'static str {
- match value {
- StorageBackend::Memory => "memory",
- StorageBackend::Sqlite => "sqlite",
- }
-}
-
-fn storage_open_mode_label(value: StorageOpenMode) -> &'static str {
- match value {
- StorageOpenMode::ReadOnly => "read_only",
- StorageOpenMode::ReadWriteExisting => "read_write_existing",
- StorageOpenMode::Create => "create",
- }
-}
-
-fn writer_policy_label(value: WriterPolicy) -> &'static str {
- match value {
- WriterPolicy::NoWriter => "no_writer",
- WriterPolicy::AdvisoryProcessLock => "advisory_process_lock",
- }
-}
-
-fn shutdown_state_label(value: ShutdownState) -> &'static str {
- match value {
- ShutdownState::Open => "open",
- ShutdownState::Closing => "closing",
- ShutdownState::Closed => "closed",
- }
-}
-
-fn integrity_health_label(value: IntegrityHealth) -> &'static str {
- match value {
- IntegrityHealth::Healthy => "healthy",
- IntegrityHealth::Degraded => "degraded",
- IntegrityHealth::Corrupt => "corrupt",
- IntegrityHealth::Unknown => "unknown",
- }
-}
-
-fn sdk_backup_manifest_preview(
- output: &Path,
- status: &StorageStatus,
- integrity: &IntegrityStatus,
-) -> Value {
- json!({
- "manifest_kind": "sdk_canonical_backup_preview",
- "destination": output.display().to_string(),
- "backup_paths": {
- "runtime_path": output.join(SDK_RUNTIME_FILE).display().to_string(),
- "private_path": output.join(SDK_PRIVATE_FILE).display().to_string(),
- },
- "source_status": status,
- "integrity": integrity,
- })
-}
-
-fn ensure_safe_sdk_backup_destination(
- config: &RuntimeConfig,
- output: &Path,
-) -> Result<(), RuntimeError> {
- let sdk_root = sdk_storage_root(config);
- let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE);
- let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE);
- let forbidden_paths = [
- sdk_root.as_path(),
- config.local.replica_store_path.as_path(),
- sdk_runtime_path.as_path(),
- sdk_private_path.as_path(),
- ];
- if forbidden_paths.contains(&output) {
- return Err(RuntimeError::Config(format!(
- "backup destination {} would overwrite canonical or derived projection store data",
- output.display()
- )));
- }
- if output.starts_with(sdk_root.as_path()) {
- return Err(RuntimeError::Config(format!(
- "backup destination {} must not be inside the SDK canonical store directory",
- output.display()
- )));
- }
- Ok(())
-}
-
-fn ensure_safe_sdk_restore_destination(
- config: &RuntimeConfig,
- destination: &Path,
-) -> Result<(), RuntimeError> {
- let sdk_root = sdk_storage_root(config);
- let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE);
- let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE);
- let forbidden_paths = [
- config.local.root.as_path(),
- config.local.replica_store_path.as_path(),
- sdk_runtime_path.as_path(),
- sdk_private_path.as_path(),
- ];
- if forbidden_paths.contains(&destination) {
- return Err(RuntimeError::Config(format!(
- "restore destination {} would overwrite canonical runtime roots or store files",
- destination.display()
- )));
- }
- if config.local.replica_store_path.starts_with(destination)
- || config.local.backups_dir.starts_with(destination)
- || config.local.exports_dir.starts_with(destination)
- {
- return Err(RuntimeError::Config(format!(
- "restore destination {} must not contain CLI runtime state directories",
- destination.display()
- )));
- }
- Ok(())
-}
-
-fn json_value(value: impl Serialize) -> Result<Value, RuntimeError> {
- serde_json::to_value(value).map_err(RuntimeError::from)
-}
-
-fn sdk_backup_plan() -> Result<BackupPlan, RuntimeError> {
- let mut bytes = [0_u8; 16];
- getrandom::getrandom(&mut bytes)
- .map_err(|error| RuntimeError::Config(format!("failed to generate backup ID: {error}")))?;
- let backup_id = BackupId::new(bytes)
- .map_err(|error| RuntimeError::Config(format!("invalid backup ID: {error}")))?;
- BackupPlan::new(
- backup_id,
- BackupFormatVersion::V1,
- BackupSecretPolicy::ExcludeProtectedStorage,
- unix_ms()?,
- )
- .map_err(|error| RuntimeError::Config(format!("invalid backup plan: {error}")))
-}
-
-fn unix_ms() -> Result<u64, RuntimeError> {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|error| RuntimeError::Config(format!("system clock error: {error}")))?
- .as_millis()
- .try_into()
- .map_err(|_| RuntimeError::Config("system clock is outside SDK range".to_owned()))
-}
-
-fn hex_bytes(bytes: &[u8]) -> String {
- const HEX: &[u8; 16] = b"0123456789abcdef";
- let mut encoded = String::with_capacity(bytes.len() * 2);
- for byte in bytes {
- encoded.push(char::from(HEX[usize::from(byte >> 4)]));
- encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
- }
- encoded
-}
-
-fn create_parent_dir(path: &Path) -> Result<(), RuntimeError> {
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)?;
- }
- Ok(())
-}
-
-fn ensure_safe_output_path(config: &RuntimeConfig, output: &Path) -> Result<(), RuntimeError> {
- if output == config.local.replica_store_path.as_path() {
- return Err(RuntimeError::Config(format!(
- "output path {} would overwrite the local replica database",
- output.display()
- )));
- }
- Ok(())
-}
-
-fn manifest_counts(manifest: &ReplicaStoreExportManifestRs) -> LocalReplicaCountsView {
- LocalReplicaCountsView {
- farms: table_row_count(manifest, "farm"),
- listings: table_row_count(manifest, "trade_product"),
- profiles: table_row_count(manifest, "nostr_profile"),
- relays: table_row_count(manifest, "nostr"),
- event_states: table_row_count(manifest, "nostr_event_state"),
- }
-}
-
-fn table_row_count(manifest: &ReplicaStoreExportManifestRs, name: &str) -> u64 {
- manifest
- .table_counts
- .iter()
- .find(|table| table.name == name)
- .map(|table| table.row_count)
- .unwrap_or(0)
-}
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -1,655 +0,0 @@
-//! CLI scheduling and presentation over the canonical shared sync engine.
-
-use std::{
- thread,
- time::{Duration, SystemTime, UNIX_EPOCH},
-};
-
-use crate::{
- cli::global::SyncWatchArgs,
- runtime::{
- RuntimeError,
- config::{RuntimeConfig, TransportProfileKind},
- sdk::{CliSdkAdapterError, CliSdkSession, sync_targets},
- },
- view::runtime::{
- SyncActionView, SyncFreshnessView, SyncQueueView, SyncStatusView,
- SyncTransportStatusInspectView, SyncTransportTargetView, SyncWatchFrameView, SyncWatchView,
- TransportOperationCapabilitiesView, TransportTargetFailureView,
- },
-};
-use radroots_replica_store::ReplicaSql;
-#[cfg(test)]
-use radroots_replica_store::migrations;
-use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
-use radroots_storage::outbox::LeaseOwner;
-use radroots_sync::{
- PullRequest, SyncStatus,
- ingest::RegistryPolicy,
- policy::SyncId,
- pull::PullTermination,
- push::{DeliveryRunReceipt, DeliveryRunRequest},
-};
-use radroots_transport::{
- SinkStatus, SourceStatus, Target,
- capability::{Availability, Maturity},
- outcome::FetchTargetState,
-};
-
-const SDK_SYNC_SOURCE: &str = "canonical SDK sync engine";
-const MARKET_FRESHNESS_STALE_AFTER_SECONDS: u64 = 15 * 60;
-const SYNC_PULL_FRESHNESS_STALE_AFTER_SECONDS: u64 = 30 * 60;
-const PULL_PAGE_LIMIT: u16 = 1_000;
-const PULL_MAX_PAGES: u16 = 5;
-const DELIVERY_LIMIT: u16 = 1_000;
-const DELIVERY_LEASE_MS: u64 = 30_000;
-
-pub fn status(config: &RuntimeConfig) -> Result<SyncStatusView, CliSdkAdapterError> {
- let session = CliSdkSession::connect(config)?;
- let receipt = canonical_status(&session)?;
- Ok(status_view(config, &receipt))
-}
-
-pub fn pull(config: &RuntimeConfig) -> Result<SyncActionView, RuntimeError> {
- pull_for_scope(config, RelayIngestScope::SyncPull)
-}
-
-pub fn market_refresh(config: &RuntimeConfig) -> Result<SyncActionView, RuntimeError> {
- pull_for_scope(config, RelayIngestScope::MarketPull)
-}
-
-fn pull_for_scope(
- config: &RuntimeConfig,
- scope: RelayIngestScope,
-) -> Result<SyncActionView, RuntimeError> {
- let session = CliSdkSession::connect(config).map_err(adapter_runtime_error)?;
- let before = canonical_status(&session).map_err(adapter_runtime_error)?;
- if !nostr_targets_configured(config) {
- return Ok(empty_action(
- config,
- &before,
- "pull",
- "unconfigured",
- Some("sync pull requires at least one configured Nostr relay".to_owned()),
- ));
- }
- if config.output.dry_run {
- let mut view = empty_action(
- config,
- &before,
- "pull",
- "dry_run",
- Some("dry run requested; canonical sync pull skipped".to_owned()),
- );
- view.target_transport_endpoints = config.transport.nostr_relay_urls.clone();
- view.fetched_count = Some(0);
- view.ingested_count = Some(0);
- view.skipped_count = Some(0);
- view.unsupported_count = Some(0);
- view.failed_count = Some(0);
- view.reason_code = Some("dry_run".to_owned());
- return Ok(view);
- }
-
- let targets = sync_targets(config).map_err(|error| RuntimeError::Config(error.to_string()))?;
- let request = PullRequest::new(targets.clone(), PULL_PAGE_LIMIT, PULL_MAX_PAGES)
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
- let operations = session
- .sdk()
- .sync()
- .map_err(CliSdkAdapterError::from)
- .map_err(adapter_runtime_error)?
- .ok_or_else(|| {
- RuntimeError::Config("canonical sync engine is not configured".to_owned())
- })?;
- let receipt = session
- .block_on(operations.pull(request, &RegistryPolicy::verified()))
- .map_err(|error| adapter_runtime_error(error.into()))?;
- let after = canonical_status(&session).map_err(adapter_runtime_error)?;
- let ingested = receipt
- .ingest_outcomes()
- .iter()
- .filter(|item| item.is_ok())
- .count();
- let ingest_failed = receipt.ingest_outcomes().len().saturating_sub(ingested);
- let mut failed_targets = Vec::new();
- for outcome in receipt.target_outcomes() {
- if matches!(outcome.state(), FetchTargetState::Complete) {
- continue;
- }
- let target = targets
- .targets()
- .iter()
- .find(|target| target.fingerprint() == outcome.target());
- failed_targets.push(TransportTargetFailureView {
- transport_kind: target
- .map_or("nostr", |target| target.kind().as_str())
- .to_owned(),
- endpoint_uri: target.map_or_else(
- || outcome.target().as_str().to_owned(),
- |target| target.uri().as_str().to_owned(),
- ),
- target_scope: target
- .and_then(|target| target.scope())
- .map(|value| value.as_str().to_owned()),
- target_label: target
- .and_then(|target| target.label())
- .map(|value| value.as_str().to_owned()),
- transport_outcome_kind: Some(fetch_state_label(outcome.state()).to_owned()),
- reason: outcome
- .message()
- .unwrap_or(fetch_state_label(outcome.state()))
- .to_owned(),
- });
- }
- let failed_count = ingest_failed + failed_targets.len();
- let state = if receipt.termination() == PullTermination::SourceFailed {
- "unavailable"
- } else if failed_count > 0 || receipt.termination() != PullTermination::Complete {
- "partial"
- } else {
- "ready"
- };
- let reason = (state != "ready").then(|| {
- format!(
- "canonical pull terminated as {} with {failed_count} failed outcome(s)",
- pull_termination_label(receipt.termination())
- )
- });
- let mut view = empty_action(config, &after, "pull", state, reason);
- view.target_transport_endpoints = config.transport.nostr_relay_urls.clone();
- view.attempted_transport_endpoints = config.transport.nostr_relay_urls.clone();
- view.accepted_transport_endpoints = config
- .transport
- .nostr_relay_urls
- .iter()
- .filter(|endpoint| {
- !failed_targets
- .iter()
- .any(|failure| &failure.endpoint_uri == *endpoint)
- })
- .cloned()
- .collect();
- view.failed_transport_targets = failed_targets;
- view.fetched_count = Some(receipt.events_observed());
- view.ingested_count = Some(ingested);
- view.skipped_count = Some(0);
- view.unsupported_count = Some(0);
- view.failed_count = Some(failed_count);
- view.reason_code =
- (state != "ready").then(|| pull_termination_label(receipt.termination()).to_owned());
- view.actions = vec![scope.ready_action().to_owned()];
- Ok(view)
-}
-
-pub fn push(config: &RuntimeConfig) -> Result<SyncActionView, CliSdkAdapterError> {
- let session = CliSdkSession::connect(config)?;
- let before = canonical_status(&session)?;
- if config.output.dry_run {
- return Ok(empty_action(
- config,
- &before,
- "push",
- "dry_run",
- Some("dry run requested; canonical outbox delivery skipped".to_owned()),
- ));
- }
- let receipt = deliver_pending(&session)?;
- let after = canonical_status(&session)?;
- Ok(delivery_action(config, &after, &receipt))
-}
-
-pub fn watch(config: &RuntimeConfig, args: &SyncWatchArgs) -> Result<SyncWatchView, RuntimeError> {
- if args.frames == 0 {
- return Err(RuntimeError::Config(
- "`sync watch --frames` must be greater than 0".to_owned(),
- ));
- }
- let mut frames = Vec::with_capacity(args.frames);
- let mut final_view = None;
- for index in 0..args.frames {
- let view = status(config).map_err(adapter_runtime_error)?;
- frames.push(SyncWatchFrameView {
- sequence: index + 1,
- observed_at: unix_now(),
- state: view.state.clone(),
- configured_transport_target_count: view.configured_transport_target_count,
- freshness: view.freshness.clone(),
- queue: view.queue.clone(),
- });
- final_view = Some(view);
- if index + 1 < args.frames {
- thread::sleep(Duration::from_millis(args.interval_ms));
- }
- }
- let view = final_view
- .ok_or_else(|| RuntimeError::Config("sync watch produced no frames".to_owned()))?;
- Ok(SyncWatchView {
- state: view.state,
- source: view.source,
- interval_ms: args.interval_ms,
- frames,
- reason: view.reason,
- actions: view.actions,
- })
-}
-
-pub(crate) fn canonical_status(session: &CliSdkSession) -> Result<SyncStatus, CliSdkAdapterError> {
- let operations = session.sdk().sync()?.ok_or_else(|| {
- RuntimeError::Config("canonical sync engine is not configured".to_owned())
- })?;
- Ok(session.block_on(operations.status(&[]))?)
-}
-
-pub(crate) fn deliver_pending(
- session: &CliSdkSession,
-) -> Result<DeliveryRunReceipt, CliSdkAdapterError> {
- let mut seed = [0_u8; 16];
- getrandom::getrandom(&mut seed).map_err(|error| {
- RuntimeError::Config(format!(
- "failed to generate delivery lease identity: {error}"
- ))
- })?;
- let owner = LeaseOwner::parse(format!("radroots-cli-{}", std::process::id()))
- .map_err(|error| RuntimeError::Config(error.to_string()))?;
- let request =
- DeliveryRunRequest::new(owner, SyncId::new(seed)?, DELIVERY_LEASE_MS, DELIVERY_LIMIT)?;
- let operations = session.sdk().sync()?.ok_or_else(|| {
- RuntimeError::Config("canonical sync engine is not configured".to_owned())
- })?;
- Ok(session.block_on(operations.deliver_pending(request))?)
-}
-
-fn status_view(config: &RuntimeConfig, status: &SyncStatus) -> SyncStatusView {
- let configured_targets = configured_targets(config);
- let state = sync_health_label(status);
- SyncStatusView {
- state: state.to_owned(),
- source: SDK_SYNC_SOURCE.to_owned(),
- local_root: config.local.root.display().to_string(),
- replica_store: "canonical".to_owned(),
- configured_transport_target_count: configured_targets.len(),
- configured_transport_targets: configured_targets,
- transport_statuses: transport_status_views(status),
- publish_policy: "canonical satisfaction policy".to_owned(),
- freshness: missing_freshness(),
- queue: queue_view(status),
- reason: (!nostr_targets_configured(config))
- .then(|| "no Nostr relay targets are configured".to_owned()),
- actions: if !nostr_targets_configured(config) {
- vec!["radroots transport config update --kind nostr --nostr-relay wss://relay.example.com".to_owned()]
- } else {
- vec![
- "radroots sync pull".to_owned(),
- "radroots sync push".to_owned(),
- ]
- },
- }
-}
-
-fn empty_action(
- config: &RuntimeConfig,
- status: &SyncStatus,
- direction: &str,
- state: &str,
- reason: Option<String>,
-) -> SyncActionView {
- let snapshot = status_view(config, status);
- SyncActionView {
- direction: direction.to_owned(),
- state: state.to_owned(),
- source: SDK_SYNC_SOURCE.to_owned(),
- local_root: snapshot.local_root,
- replica_store: snapshot.replica_store,
- configured_transport_target_count: snapshot.configured_transport_target_count,
- configured_transport_targets: snapshot.configured_transport_targets,
- transport_statuses: snapshot.transport_statuses,
- publish_policy: snapshot.publish_policy,
- freshness: snapshot.freshness,
- queue: snapshot.queue,
- target_transport_endpoints: Vec::new(),
- attempted_transport_endpoints: Vec::new(),
- accepted_transport_endpoints: Vec::new(),
- failed_transport_targets: Vec::new(),
- fetched_count: None,
- ingested_count: None,
- publishable_count: None,
- published_count: None,
- skipped_count: None,
- unsupported_count: None,
- failed_count: None,
- publish_plan: None,
- reason_code: None,
- reason,
- actions: snapshot.actions,
- }
-}
-
-fn delivery_action(
- config: &RuntimeConfig,
- status: &SyncStatus,
- receipt: &DeliveryRunReceipt,
-) -> SyncActionView {
- let attempted = receipt.outcomes().len();
- let succeeded = receipt.succeeded();
- let failed = receipt.failed();
- let state = if attempted == 0 {
- "ready"
- } else if failed == 0 {
- "published"
- } else if succeeded > 0 {
- "partial"
- } else {
- "unavailable"
- };
- let mut view = empty_action(
- config,
- status,
- "push",
- state,
- (failed > 0).then(|| format!("{failed} canonical delivery outcome(s) failed")),
- );
- view.target_transport_endpoints = config.transport.nostr_relay_urls.clone();
- view.attempted_transport_endpoints = if attempted > 0 {
- config.transport.nostr_relay_urls.clone()
- } else {
- Vec::new()
- };
- view.accepted_transport_endpoints = if succeeded > 0 {
- config.transport.nostr_relay_urls.clone()
- } else {
- Vec::new()
- };
- view.publishable_count = Some(attempted);
- view.published_count = Some(succeeded);
- view.failed_count = Some(failed);
- view.reason_code = (failed > 0).then(|| "delivery_partial_failure".to_owned());
- view.actions = vec!["radroots sync status".to_owned()];
- view
-}
-
-fn queue_view(status: &SyncStatus) -> SyncQueueView {
- let outbox = status.outbox();
- let total = outbox.total().and_then(|value| usize::try_from(value).ok());
- SyncQueueView {
- expected_count: total.unwrap_or_default(),
- pending_count: usize::try_from(outbox.pending + outbox.leased + outbox.retryable)
- .unwrap_or(usize::MAX),
- total_count: total,
- retryable_count: usize::try_from(outbox.retryable).ok(),
- terminal_count: usize::try_from(outbox.satisfied + outbox.exhausted).ok(),
- failed_terminal_count: usize::try_from(outbox.exhausted).ok(),
- deferred_until_implemented_count: Some(0),
- ready_signed_count: usize::try_from(outbox.pending + outbox.retryable).ok(),
- publishing_count: usize::try_from(outbox.leased).ok(),
- last_attempt_at_ms: None,
- last_error: None,
- }
-}
-
-fn configured_targets(config: &RuntimeConfig) -> Vec<SyncTransportTargetView> {
- config
- .transport
- .nostr_relay_urls
- .iter()
- .filter_map(|endpoint| Target::nostr_relay(endpoint).ok())
- .map(|target| SyncTransportTargetView {
- transport_kind: target.kind().as_str().to_owned(),
- endpoint_uri: target.uri().as_str().to_owned(),
- endpoint_fingerprint: target.fingerprint().as_str().to_owned(),
- target_scope: target.scope().map(|scope| scope.as_str().to_owned()),
- target_label: target.label().map(|label| label.as_str().to_owned()),
- })
- .collect()
-}
-
-fn transport_status_views(status: &SyncStatus) -> Vec<SyncTransportStatusInspectView> {
- let mut views = Vec::new();
- if let Some(source) = status.source().status() {
- views.push(source_status_view(source));
- }
- if let Some(sink) = status.sink().status()
- && !views
- .iter()
- .any(|view| view.transport == sink.transport_id().as_str())
- {
- views.push(sink_status_view(sink));
- }
- views
-}
-
-fn source_status_view(status: &SourceStatus) -> SyncTransportStatusInspectView {
- SyncTransportStatusInspectView {
- transport: status.transport_id().as_str().to_owned(),
- profile_id: None,
- endpoint_uri: None,
- configured: status.is_configured(),
- implementation: "real".to_owned(),
- maturity: maturity_label(status.maturity()).to_owned(),
- availability: availability_label(status.availability()).to_owned(),
- usable_for_delivery: false,
- capabilities: TransportOperationCapabilitiesView {
- deliver: false,
- fetch: status.capabilities().can_fetch(),
- },
- message: status.message().to_owned(),
- }
-}
-
-fn sink_status_view(status: &SinkStatus) -> SyncTransportStatusInspectView {
- SyncTransportStatusInspectView {
- transport: status.transport_id().as_str().to_owned(),
- profile_id: None,
- endpoint_uri: None,
- configured: status.is_configured(),
- implementation: "real".to_owned(),
- maturity: maturity_label(status.maturity()).to_owned(),
- availability: availability_label(status.availability()).to_owned(),
- usable_for_delivery: status.capabilities().can_deliver()
- && status.availability() != Availability::Unavailable,
- capabilities: TransportOperationCapabilitiesView {
- deliver: status.capabilities().can_deliver(),
- fetch: false,
- },
- message: status.message().to_owned(),
- }
-}
-
-fn sync_health_label(status: &SyncStatus) -> &'static str {
- match status.health() {
- radroots_protocol::runtime::v1::SyncHealth::Healthy => "ready",
- radroots_protocol::runtime::v1::SyncHealth::Degraded => "degraded",
- radroots_protocol::runtime::v1::SyncHealth::Unavailable => "unavailable",
- }
-}
-
-fn maturity_label(value: Maturity) -> &'static str {
- match value {
- Maturity::Experimental => "experimental",
- Maturity::Preview => "preview",
- Maturity::Stable => "stable",
- }
-}
-
-fn availability_label(value: Availability) -> &'static str {
- match value {
- Availability::Available => "available",
- Availability::Degraded => "degraded",
- Availability::Unavailable => "unavailable",
- }
-}
-
-fn fetch_state_label(value: FetchTargetState) -> &'static str {
- match value {
- FetchTargetState::Complete => "complete",
- FetchTargetState::Partial => "partial",
- FetchTargetState::Unavailable => "unavailable",
- FetchTargetState::FailedRetryable => "failed_retryable",
- FetchTargetState::FailedTerminal => "failed_terminal",
- FetchTargetState::Cancelled => "cancelled",
- }
-}
-
-fn pull_termination_label(value: PullTermination) -> &'static str {
- match value {
- PullTermination::Complete => "complete",
- PullTermination::PageLimit => "page_limit",
- PullTermination::Deadline => "deadline",
- PullTermination::Cancelled => "cancelled",
- PullTermination::SourceFailed => "source_failed",
- }
-}
-
-fn nostr_targets_configured(config: &RuntimeConfig) -> bool {
- matches!(
- config.transport.profile,
- TransportProfileKind::Nostr | TransportProfileKind::MultiTarget
- ) && !config.transport.nostr_relay_urls.is_empty()
-}
-
-fn adapter_runtime_error(error: CliSdkAdapterError) -> RuntimeError {
- RuntimeError::Network(error.to_string())
-}
-
-pub(crate) fn missing_freshness() -> SyncFreshnessView {
- SyncFreshnessView {
- state: "never".to_owned(),
- display: "never synced".to_owned(),
- age_seconds: None,
- last_event_at: None,
- run: None,
- }
-}
-
-#[cfg(test)]
-pub(crate) fn freshness_for_scope(
- config: &RuntimeConfig,
- scope: RelayIngestScope,
-) -> Result<SyncFreshnessView, RuntimeError> {
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- migrations::run_all_up(&executor)?;
- freshness_for_scope_from_executor(config, &executor, scope)
-}
-
-#[cfg(test)]
-pub(crate) fn relay_provenance_relays_for_scope(
- _config: &RuntimeConfig,
- _scope: RelayIngestScope,
-) -> Result<Vec<String>, RuntimeError> {
- Ok(Vec::new())
-}
-
-pub(crate) fn freshness_for_scope_from_executor(
- _config: &RuntimeConfig,
- executor: &SqlxSqliteExecutor,
- scope: RelayIngestScope,
-) -> Result<SyncFreshnessView, RuntimeError> {
- let last_event_at = ReplicaSql::new(executor).nostr_event_last_created_at()?;
- let age_seconds = last_event_at.map(|last| unix_now().saturating_sub(last));
- let state = match age_seconds {
- None => "never",
- Some(age) if age > scope.stale_after_seconds() => "stale",
- Some(_) => "fresh",
- };
- Ok(SyncFreshnessView {
- state: state.to_owned(),
- display: match age_seconds {
- Some(age) => format!("{} {state} {}s ago", scope.display(), age),
- None => format!("{} never synced", scope.display()),
- },
- age_seconds,
- last_event_at,
- run: None,
- })
-}
-
-pub(crate) fn freshness_requires_refresh(freshness: &SyncFreshnessView) -> bool {
- matches!(
- freshness.state.as_str(),
- "never" | "stale" | "relay_set_changed" | "refresh_failed"
- )
-}
-
-pub(crate) fn ensure_sync_run_table(executor: &SqlxSqliteExecutor) -> Result<(), RuntimeError> {
- executor.exec(
- "CREATE TABLE IF NOT EXISTS radroots_cli_sync_run (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- scope TEXT NOT NULL,
- relay_set_fingerprint TEXT NOT NULL,
- target_transport_endpoints_json TEXT NOT NULL,
- attempted_transport_endpoints_json TEXT NOT NULL,
- failed_transport_targets_json TEXT NOT NULL,
- started_at INTEGER NOT NULL,
- completed_at INTEGER,
- state TEXT NOT NULL,
- fetched_count INTEGER NOT NULL,
- ingested_count INTEGER NOT NULL,
- skipped_count INTEGER NOT NULL,
- unsupported_count INTEGER NOT NULL,
- failed_count INTEGER NOT NULL,
- failure_reason TEXT
- );
- CREATE INDEX IF NOT EXISTS idx_radroots_cli_sync_run_scope_started
- ON radroots_cli_sync_run(scope, started_at DESC);",
- "[]",
- )?;
- Ok(())
-}
-
-#[derive(Debug, Clone, Copy)]
-pub(crate) enum RelayIngestScope {
- SyncPull,
- MarketPull,
-}
-
-impl RelayIngestScope {
- fn display(self) -> &'static str {
- match self {
- Self::SyncPull => "sync pull",
- Self::MarketPull => "market refresh",
- }
- }
-
- fn stale_after_seconds(self) -> u64 {
- match self {
- Self::SyncPull => SYNC_PULL_FRESHNESS_STALE_AFTER_SECONDS,
- Self::MarketPull => MARKET_FRESHNESS_STALE_AFTER_SECONDS,
- }
- }
-
- fn ready_action(self) -> &'static str {
- match self {
- Self::SyncPull => "radroots market search eggs",
- Self::MarketPull => "radroots market search eggs",
- }
- }
-}
-
-fn unix_now() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_or(0, |duration| duration.as_secs())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn receipt_labels_cover_partial_failure_and_cancellation() {
- assert_eq!(
- pull_termination_label(PullTermination::Cancelled),
- "cancelled"
- );
- assert_eq!(
- fetch_state_label(FetchTargetState::FailedRetryable),
- "failed_retryable"
- );
- assert_eq!(fetch_state_label(FetchTargetState::Partial), "partial");
- }
-
- #[test]
- fn missing_freshness_requires_a_pull() {
- assert!(freshness_requires_refresh(&missing_freshness()));
- }
-}
diff --git a/src/runtime/trade.rs b/src/runtime/trade.rs
@@ -1,985 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicU64, Ordering};
-use std::time::{SystemTime, UNIX_EPOCH};
-
-use radroots_core::unit::convert_unit_decimal;
-use radroots_core::{Currency, Decimal, Unit};
-use radroots_event::contract::AuthorRole;
-use radroots_event::id::{
- CandidateId, ClassifiedListingAddress, DTag, InventoryBinId, MutationId, TradeId,
-};
-use radroots_event::trade::{
- FulfillmentProfileV1, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION,
- TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1,
- TradeEconomicsProfileV1, TradeMutationBodyV1, TradeMutationEnvelopeV1,
- canonical_trade_mutation_content,
-};
-use radroots_identity::PublicKey;
-use radroots_replica_schema::nostr_event_head::{
- INostrEventHeadFindOne, INostrEventHeadFindOneArgs, NostrEventHeadQueryBindValues,
-};
-use radroots_replica_schema::trade_product::{ITradeProductFieldsFilter, ITradeProductFindMany};
-use radroots_replica_store::{ReplicaSql, nostr_event_head, trade_product};
-use radroots_sdk::trade::{self as sdk_trade, Plan as TradePlan};
-use radroots_signing::{Actor, actor::ActorSource};
-use radroots_sql_core::SqlxSqliteExecutor;
-use serde::Serialize;
-
-use crate::runtime::RuntimeError;
-use crate::runtime::account;
-use crate::runtime::config::RuntimeConfig;
-use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor};
-
-const TRADE_CANDIDATE_DRAFTS_DIR: &str = "trades/candidates";
-const TRADE_CANDIDATE_DRAFT_SOURCE: &str = "SDK trade proposal candidate draft";
-const DEFAULT_PROPOSAL_TTL_SECONDS: u64 = 7 * 24 * 60 * 60;
-const DEFAULT_FULFILLMENT_START_OFFSET_SECONDS: u64 = 24 * 60 * 60;
-const DEFAULT_FULFILLMENT_WINDOW_SECONDS: u64 = 2 * 60 * 60;
-
-static TRADE_COUNTER: AtomicU64 = AtomicU64::new(0);
-
-#[derive(Debug, Clone)]
-pub struct TradeEnvelopeFileRuntimeArgs {
- pub file: PathBuf,
- pub idempotency_key: Option<String>,
- pub acknowledge_private_terms: bool,
- pub operation_kind: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradeIdRuntimeArgs {
- pub trade_id: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradePageRuntimeArgs {
- pub limit: Option<u32>,
- pub cursor: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradeEvidenceInspectRuntimeArgs {
- pub trade_id: String,
- pub limit: Option<u32>,
- pub cursor: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradePrivateArtifactSealRuntimeArgs {
- pub trade_id: String,
- pub artifact_id: String,
- pub schema_id: String,
- pub input: PathBuf,
- pub kind: String,
- pub candidate_id: Option<String>,
- pub retention_class: Option<String>,
- pub expires_at_ms: Option<i64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradePrivateArtifactOpenRuntimeArgs {
- pub artifact_id: String,
- pub output: PathBuf,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradePrivateArtifactDeleteRuntimeArgs {
- pub artifact_id: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct TradeCandidateDraftCreateArgs {
- pub listing: Option<String>,
- pub listing_addr: Option<String>,
- pub bin_id: String,
- pub quantity: u32,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TradeCandidateDraftView {
- pub state: String,
- pub source: String,
- pub trade_id: String,
- pub candidate_id: Option<String>,
- pub mutation_id: Option<String>,
- pub file: String,
- pub listing_addr: String,
- pub listing_event_id: String,
- pub listing_snapshot_sha256: String,
- pub buyer_pubkey: String,
- pub seller_pubkey: String,
- pub farm_id: String,
- pub ready_for_submit: bool,
- pub economics: TradeCandidateDraftEconomicsView,
- pub issues: Vec<TradeCandidateDraftIssue>,
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TradeCandidateDraftEconomicsView {
- pub currency_code: String,
- pub currency_exponent: u32,
- pub subtotal_mantissa: String,
- pub discount_total_mantissa: String,
- pub adjustment_total_mantissa: String,
- pub total_mantissa: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TradeCandidateDraftIssue {
- pub code: String,
- pub field: String,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TradePrivateArtifactOpenView {
- pub state: String,
- pub artifact_id: String,
- pub trade_id: Option<String>,
- pub candidate_id: Option<String>,
- pub artifact_kind: Option<String>,
- pub schema_id: Option<String>,
- pub retention_class: Option<String>,
- pub output: String,
- pub bytes_written: usize,
- pub created_at_ms: Option<i64>,
- pub expires_at_ms: Option<i64>,
- pub deleted_at_ms: Option<i64>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TradePreparedView {
- pub state: String,
- pub operation: String,
- pub trade_id: String,
- pub mutation_id: String,
- pub mutation_kind: String,
- pub event_id: String,
- pub event_kind: u32,
- pub author: String,
- pub required_actions: Vec<String>,
- pub idempotency_key: Option<String>,
- pub reason: Option<String>,
- pub actions: Vec<String>,
-}
-
-pub fn submit_proposal(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- prepare_trade_command(config, args, "trade.submit_proposal.v1")
-}
-
-pub fn propose_revision(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- prepare_trade_command(config, args, "trade.propose_revision.v1")
-}
-
-pub fn decide_candidate(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- prepare_trade_command(config, args, "trade.decide_candidate.v1")
-}
-
-pub fn cancel_trade(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- prepare_trade_command(config, args, "trade.cancel.v1")
-}
-
-pub fn resume_operation(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- let operation_kind = args.operation_kind.as_deref().ok_or_else(|| {
- RuntimeError::Config("trade operation resume requires `--operation-kind`".to_owned())
- })?;
- prepare_trade_command(config, args, resume_operation_kind(operation_kind)?)
-}
-
-fn prepare_trade_command(
- config: &RuntimeConfig,
- args: &TradeEnvelopeFileRuntimeArgs,
- operation: &str,
-) -> Result<TradePreparedView, CliSdkAdapterError> {
- let envelope = load_trade_envelope(args.file.as_path())?;
- let actor = actor_for_envelope(config, &envelope, operation)?;
- let idempotency_key = args
- .idempotency_key
- .as_deref()
- .and_then(non_empty_ref)
- .map(radroots_storage::journal::IdempotencyKey::parse)
- .transpose()
- .map_err(|error| RuntimeError::Config(format!("invalid idempotency key: {error}")))?;
- let plan = sdk_trade::prepare(sdk_trade::PrepareRequest::new(actor, envelope))
- .map_err(|error| RuntimeError::Config(format!("invalid SDK trade plan: {error}")))?;
- if matches!(operation, "trade.decide_candidate.v1")
- && plan.workflow().private_terms().is_some()
- && !args.acknowledge_private_terms
- {
- return Err(RuntimeError::Config(
- "trade decision requires explicit private-terms acknowledgement".to_owned(),
- )
- .into());
- }
- Ok(trade_prepared_view(operation, args, &plan, idempotency_key))
-}
-
-fn trade_prepared_view(
- operation: &str,
- args: &TradeEnvelopeFileRuntimeArgs,
- plan: &TradePlan,
- idempotency_key: Option<radroots_storage::journal::IdempotencyKey>,
-) -> TradePreparedView {
- TradePreparedView {
- state: "prepared".to_owned(),
- operation: operation.to_owned(),
- trade_id: plan.workflow().trade_id().to_string(),
- mutation_id: plan.workflow().mutation_id().to_string(),
- mutation_kind: format!("{:?}", plan.workflow().kind()),
- event_id: plan.draft().expected_event_id().to_string(),
- event_kind: plan.draft().kind_u32(),
- author: plan.draft().expected_pubkey().to_hex(),
- required_actions: plan
- .workflow()
- .required_actions()
- .iter()
- .map(|action| format!("{action:?}"))
- .collect(),
- idempotency_key: idempotency_key.map(|value| value.as_str().to_owned()),
- reason: Some(
- "trade plan validated; durable enqueue requires the configured shared sync engine"
- .to_owned(),
- ),
- actions: vec![format!(
- "radroots trade operation resume {}",
- args.file.display()
- )],
- }
-}
-
-pub fn get_trade(
- _config: &RuntimeConfig,
- args: &TradeIdRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- let _ = trade_id(args.trade_id.as_str(), "trade_id")?;
- Err(RuntimeError::Config(
- "trade queries require the configured SDK storage and projection adapter".to_owned(),
- )
- .into())
-}
-
-pub fn list_trades(
- _config: &RuntimeConfig,
- args: &TradePageRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- if matches!(args.limit, Some(0)) {
- return Err(RuntimeError::Config("trade list limit must be positive".to_owned()).into());
- }
- let _ = args.cursor.as_deref().and_then(non_empty_ref);
- Err(RuntimeError::Config(
- "trade queries require the configured SDK storage and projection adapter".to_owned(),
- )
- .into())
-}
-
-pub fn refresh_evidence(
- _config: &RuntimeConfig,
- args: &TradeIdRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- let _ = trade_id(args.trade_id.as_str(), "trade_id")?;
- Err(RuntimeError::Config(
- "trade projection refresh requires the configured shared sync engine".to_owned(),
- )
- .into())
-}
-
-pub fn inspect_evidence(
- _config: &RuntimeConfig,
- args: &TradeEvidenceInspectRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- let _ = trade_id(args.trade_id.as_str(), "trade_id")?;
- if matches!(args.limit, Some(0)) {
- return Err(
- RuntimeError::Config("trade evidence limit must be positive".to_owned()).into(),
- );
- }
- let _ = args.cursor.as_deref().and_then(non_empty_ref);
- Err(RuntimeError::Config(
- "trade evidence queries require the configured SDK storage adapter".to_owned(),
- )
- .into())
-}
-
-pub fn seal_private_artifact(
- _config: &RuntimeConfig,
- args: &TradePrivateArtifactSealRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- let _ = trade_id(args.trade_id.as_str(), "trade_id")?;
- let _ = private_artifact_kind(args.kind.as_str())?;
- let _ = fs::metadata(args.input.as_path()).map_err(RuntimeError::from)?;
- Err(RuntimeError::Config(
- "private trade artifact sealing requires a host-owned secret adapter".to_owned(),
- )
- .into())
-}
-
-pub fn open_private_artifact(
- _config: &RuntimeConfig,
- args: &TradePrivateArtifactOpenRuntimeArgs,
-) -> Result<TradePrivateArtifactOpenView, CliSdkAdapterError> {
- Err(RuntimeError::Config(format!(
- "private trade artifact `{}` requires a host-owned secret adapter",
- args.artifact_id
- ))
- .into())
-}
-
-pub fn delete_private_artifact(
- _config: &RuntimeConfig,
- args: &TradePrivateArtifactDeleteRuntimeArgs,
-) -> Result<serde_json::Value, CliSdkAdapterError> {
- Err(RuntimeError::Config(format!(
- "private trade artifact `{}` requires a host-owned secret adapter",
- args.artifact_id
- ))
- .into())
-}
-
-pub fn scaffold_proposal_draft(
- config: &RuntimeConfig,
- args: &TradeCandidateDraftCreateArgs,
-) -> Result<TradeCandidateDraftView, RuntimeError> {
- scaffold_proposal_draft_inner(config, args, false)
-}
-
-pub fn scaffold_proposal_draft_preflight(
- config: &RuntimeConfig,
- args: &TradeCandidateDraftCreateArgs,
-) -> Result<TradeCandidateDraftView, RuntimeError> {
- scaffold_proposal_draft_inner(config, args, true)
-}
-
-fn scaffold_proposal_draft_inner(
- config: &RuntimeConfig,
- args: &TradeCandidateDraftCreateArgs,
- dry_run: bool,
-) -> Result<TradeCandidateDraftView, RuntimeError> {
- if args.quantity == 0 {
- return Err(RuntimeError::Config(
- "basket item quantity must be greater than zero".to_owned(),
- ));
- }
- let buyer = account::resolve_account(config)?.ok_or_else(|| {
- RuntimeError::Config("trade proposal draft requires a selected buyer account".to_owned())
- })?;
- let product = resolve_product(config, args)?;
- let parsed_listing = parse_listing_addr(product.listing_addr.as_str())?;
- let listing_state =
- resolve_active_listing_state(config, product.listing_addr.as_str(), &parsed_listing)?;
- let farm_id = resolve_farm_id(config, parsed_listing.seller_pubkey.as_str())?;
- let buyer_public_key_hex = buyer.record.public_identity().public_key().to_hex();
- let buyer_pubkey = pubkey(buyer_public_key_hex.as_str(), "buyer_pubkey")?;
- let seller_pubkey = pubkey(parsed_listing.seller_pubkey.as_str(), "seller_pubkey")?;
- let candidate = candidate_terms(
- &product,
- &listing_state,
- buyer_pubkey.clone(),
- seller_pubkey.clone(),
- farm_id.clone(),
- args.quantity,
- )?;
- let envelope = TradeMutationEnvelopeV1 {
- mutation_id: None,
- contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- trade_id: next_trade_id()?,
- root_mutation_id: None,
- buyer_pubkey,
- seller_pubkey: seller_pubkey.clone(),
- farm_id: farm_id.clone(),
- parent_mutation_ids: Vec::new(),
- author_pubkey: pubkey(buyer_public_key_hex.as_str(), "author_pubkey")?,
- counterparty_pubkey: seller_pubkey,
- authored_at_unix_s: now_unix(),
- body: TradeMutationBodyV1::Proposal { candidate },
- };
- let canonical = canonical_trade_mutation_content(envelope)
- .map_err(|error| RuntimeError::Config(format!("build trade proposal envelope: {error}")))?;
- let file = candidate_draft_file(config, canonical.envelope.trade_id.to_string().as_str());
- if !dry_run {
- if let Some(parent) = file.parent() {
- fs::create_dir_all(parent)?;
- }
- let contents = serde_json::to_string_pretty(&canonical.envelope)?;
- fs::write(file.as_path(), contents)?;
- }
- let economics = proposal_economics_view(&canonical.envelope);
- let candidate_id = proposal_candidate_id(&canonical.envelope);
- Ok(TradeCandidateDraftView {
- state: if dry_run { "dry_run" } else { "drafted" }.to_owned(),
- source: TRADE_CANDIDATE_DRAFT_SOURCE.to_owned(),
- trade_id: canonical.envelope.trade_id.to_string(),
- candidate_id,
- mutation_id: canonical
- .envelope
- .mutation_id
- .as_ref()
- .map(ToString::to_string),
- file: file.display().to_string(),
- listing_addr: product.listing_addr,
- listing_event_id: listing_state.last_event_id,
- listing_snapshot_sha256: listing_state.content_hash,
- buyer_pubkey: buyer_public_key_hex,
- seller_pubkey: parsed_listing.seller_pubkey,
- farm_id: farm_id.to_string(),
- ready_for_submit: true,
- economics,
- issues: Vec::new(),
- actions: if dry_run {
- Vec::new()
- } else {
- vec![format!("radroots trade proposal submit {}", file.display())]
- },
- })
-}
-
-fn load_trade_envelope(path: &Path) -> Result<TradeMutationEnvelopeV1, RuntimeError> {
- let contents = fs::read_to_string(path)?;
- serde_json::from_str(contents.as_str()).map_err(|error| {
- RuntimeError::Config(format!(
- "invalid trade envelope {}: {error}",
- path.display()
- ))
- })
-}
-
-fn actor_for_envelope(
- config: &RuntimeConfig,
- envelope: &TradeMutationEnvelopeV1,
- operation: &str,
-) -> Result<Actor, CliSdkAdapterError> {
- let account = account::resolve_account(config)?.ok_or_else(|| {
- RuntimeError::Config(format!("{operation} requires a selected signer account"))
- })?;
- let author_pubkey = envelope.author_pubkey.to_hex();
- let account_pubkey = account.record.public_identity().public_key().to_hex();
- if !account_pubkey.eq_ignore_ascii_case(author_pubkey.as_str()) {
- return Err(RuntimeError::Config(format!(
- "{operation} envelope author `{author_pubkey}` does not match selected account `{}` public key `{account_pubkey}`",
- account.record.id()
- ))
- .into());
- }
- let role = if envelope
- .buyer_pubkey
- .to_hex()
- .eq_ignore_ascii_case(author_pubkey.as_str())
- {
- AuthorRole::Buyer
- } else if envelope
- .seller_pubkey
- .to_hex()
- .eq_ignore_ascii_case(author_pubkey.as_str())
- {
- AuthorRole::Seller
- } else {
- return Err(RuntimeError::Config(format!(
- "{operation} envelope author must be the buyer or seller"
- ))
- .into());
- };
- let actor = Actor::from_public_key_hex(
- author_pubkey.as_str(),
- ActorSource::ExplicitPublicKey,
- [role],
- )
- .map_err(|error| RuntimeError::Config(format!("invalid trade SDK actor: {error}")))?;
- validate_configured_signer_for_actor(
- config,
- Some(account.record.id().to_hex().as_str()),
- author_pubkey.as_str(),
- operation,
- )?;
- Ok(actor)
-}
-
-fn resume_operation_kind(value: &str) -> Result<&'static str, RuntimeError> {
- match value {
- "trade.submit_proposal.v1" => Ok("trade.submit_proposal.v1"),
- "trade.propose_revision.v1" => Ok("trade.propose_revision.v1"),
- "trade.decide_candidate.v1" => Ok("trade.decide_candidate.v1"),
- "trade.cancel.v1" => Ok("trade.cancel.v1"),
- other => Err(RuntimeError::Config(format!(
- "unsupported trade operation kind `{other}`"
- ))),
- }
-}
-
-fn private_artifact_kind(value: &str) -> Result<&'static str, RuntimeError> {
- match value {
- "binding_terms" => Ok("binding_terms"),
- "message" => Ok("message"),
- "contact_bundle" => Ok("contact_bundle"),
- "delivery_instruction" => Ok("delivery_instruction"),
- other => Err(RuntimeError::Config(format!(
- "unsupported private artifact kind `{other}`"
- ))),
- }
-}
-
-#[derive(Debug, Clone)]
-struct ProductFacts {
- key: String,
- qty_amt_exact: String,
- qty_unit: String,
- price_amt_exact: String,
- price_currency: String,
- price_qty_amt_exact: String,
- price_qty_unit: String,
- listing_addr: String,
- primary_bin_id: String,
-}
-
-#[derive(Debug, Clone)]
-struct ParsedListingAddress {
- kind: u32,
- seller_pubkey: String,
- listing_id: String,
-}
-
-#[derive(Debug, Clone)]
-struct ActiveListingState {
- last_event_id: String,
- content_hash: String,
-}
-
-fn resolve_product(
- config: &RuntimeConfig,
- args: &TradeCandidateDraftCreateArgs,
-) -> Result<ProductFacts, RuntimeError> {
- if !config.local.replica_store_path.exists() {
- return Err(RuntimeError::Config(
- "trade proposal draft requires local market data; run `radroots store inspect` and `radroots market pull`".to_owned(),
- ));
- }
- if let Some(listing_addr) = args.listing_addr.as_deref().and_then(non_empty_ref) {
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- let rows = trade_product::find_many(
- &executor,
- &ITradeProductFindMany {
- filter: Some(trade_product_listing_addr_filter(listing_addr)),
- },
- )
- .map_err(|error| RuntimeError::Config(format!("resolve listing product state: {error:?}")))?
- .results;
- let product = match rows.len() {
- 0 => {
- return Err(RuntimeError::Config(format!(
- "listing address `{listing_addr}` is not available in the local replica"
- )));
- }
- 1 => rows.into_iter().next().expect("one row"),
- count => {
- return Err(RuntimeError::Config(format!(
- "listing address `{listing_addr}` matched {count} local listing rows"
- )));
- }
- };
- let facts = ProductFacts {
- key: product.key,
- qty_amt_exact: required(product.qty_amt_exact, "qty_amt_exact")?,
- qty_unit: product.qty_unit,
- price_amt_exact: required(product.price_amt_exact, "price_amt_exact")?,
- price_currency: product.price_currency,
- price_qty_amt_exact: required(product.price_qty_amt_exact, "price_qty_amt_exact")?,
- price_qty_unit: product.price_qty_unit,
- listing_addr: required(product.listing_addr, "listing_addr")?,
- primary_bin_id: verified_primary_bin(
- product.primary_bin_id,
- product.verified_primary_bin_id,
- )?,
- };
- validate_product_bin(&facts, args.bin_id.as_str())?;
- return Ok(facts);
- }
- let listing = args
- .listing
- .as_deref()
- .and_then(non_empty_ref)
- .ok_or_else(|| {
- RuntimeError::Config(
- "trade proposal draft requires `listing` or `listing_addr`".to_owned(),
- )
- })?;
- let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?);
- let rows = db.trade_product_lookup(listing)?;
- let product = match rows.len() {
- 0 => {
- return Err(RuntimeError::Config(format!(
- "listing `{listing}` is not available in the local replica"
- )));
- }
- 1 => rows.into_iter().next().expect("one row"),
- count => {
- return Err(RuntimeError::Config(format!(
- "listing `{listing}` matched {count} local listings"
- )));
- }
- };
- let facts = ProductFacts {
- key: product.key,
- qty_amt_exact: required(product.qty_amt_exact, "qty_amt_exact")?,
- qty_unit: product.qty_unit,
- price_amt_exact: required(product.price_amt_exact, "price_amt_exact")?,
- price_currency: product.price_currency,
- price_qty_amt_exact: required(product.price_qty_amt_exact, "price_qty_amt_exact")?,
- price_qty_unit: product.price_qty_unit,
- listing_addr: required(product.listing_addr, "listing_addr")?,
- primary_bin_id: verified_primary_bin(
- product.primary_bin_id,
- product.verified_primary_bin_id,
- )?,
- };
- validate_product_bin(&facts, args.bin_id.as_str())?;
- Ok(facts)
-}
-
-fn candidate_terms(
- product: &ProductFacts,
- listing_state: &ActiveListingState,
- buyer_pubkey: PublicKey,
- seller_pubkey: PublicKey,
- farm_id: DTag,
- bin_count: u32,
-) -> Result<TradeCandidateTermsV1, RuntimeError> {
- let currency = product
- .price_currency
- .parse::<Currency>()
- .map_err(|error| {
- RuntimeError::Config(format!("listing price_currency is invalid: {error}"))
- })?;
- let quantity_amount = exact_positive_decimal(product.qty_amt_exact.as_str(), "qty_amt_exact")?
- .checked_mul(Decimal::from(bin_count))
- .map_err(|error| RuntimeError::Config(format!("trade quantity overflow: {error}")))?;
- let quantity_unit = product
- .qty_unit
- .parse::<Unit>()
- .map_err(|error| RuntimeError::Config(format!("listing qty_unit is invalid: {error}")))?;
- let price_amount = exact_positive_decimal(product.price_amt_exact.as_str(), "price_amt_exact")?;
- let price_quantity_amount =
- exact_positive_decimal(product.price_qty_amt_exact.as_str(), "price_qty_amt_exact")?;
- let price_unit = product.price_qty_unit.parse::<Unit>().map_err(|error| {
- RuntimeError::Config(format!("listing price_qty_unit is invalid: {error}"))
- })?;
- let quantity_unit_in_price_units =
- convert_unit_decimal(Decimal::ONE, quantity_unit, price_unit).map_err(|error| {
- RuntimeError::Config(format!(
- "listing quantity and price units are incompatible: {error}"
- ))
- })?;
- let unit_price_amount = price_amount
- .checked_div(price_quantity_amount)
- .and_then(|value| value.checked_mul(quantity_unit_in_price_units))
- .map_err(|error| RuntimeError::Config(format!("trade unit price is invalid: {error}")))?;
- let subtotal = unit_price_amount
- .checked_mul(quantity_amount)
- .map_err(|error| RuntimeError::Config(format!("trade subtotal overflow: {error}")))?;
- let quantity_scale = u8::try_from(quantity_amount.scale())
- .map_err(|_| RuntimeError::Config("trade quantity scale exceeds u8".to_owned()))?;
- let currency_exponent = currency.minor_unit_exponent();
- let subtotal_mantissa = decimal_mantissa_at_scale(subtotal, currency_exponent);
- let line = TradeCandidateLineV1 {
- line_id: DTag::parse("line-1")
- .map_err(|error| RuntimeError::Config(format!("invalid line id: {error}")))?,
- listing_addr: ClassifiedListingAddress::parse(product.listing_addr.as_str())
- .map_err(|error| RuntimeError::Config(format!("invalid listing address: {error}")))?,
- listing_event_id: listing_state
- .last_event_id
- .parse()
- .map_err(|error| RuntimeError::Config(format!("invalid listing event id: {error}")))?,
- listing_snapshot_sha256: listing_state.content_hash.clone(),
- product_id: product.key.clone(),
- option_id: None,
- bin_id: inventory_bin_id(product.primary_bin_id.as_str(), "bin_id")?,
- quantity_mantissa: decimal_mantissa_at_scale(quantity_amount, u32::from(quantity_scale)),
- quantity_scale,
- unit_code: product.qty_unit.clone(),
- unit_profile: "radroots-core-unit".to_owned(),
- unit_price_mantissa: decimal_mantissa_at_scale(unit_price_amount, currency_exponent),
- currency_code: product.price_currency.clone(),
- line_subtotal_mantissa: subtotal_mantissa.clone(),
- replaces_line_id: None,
- };
- let now = now_unix();
- Ok(TradeCandidateTermsV1 {
- candidate_id: None,
- schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
- base_candidate_id: None,
- supersession_intent: None,
- buyer_pubkey,
- seller_pubkey,
- farm_id,
- lines: vec![line],
- line_tombstones: Vec::new(),
- economics: TradeEconomicsProfileV1 {
- profile_id: "mvp-fixed".to_owned(),
- currency_code: product.price_currency.clone(),
- currency_exponent: u8::try_from(currency_exponent)
- .map_err(|_| RuntimeError::Config("currency exponent exceeds u8".to_owned()))?,
- rounding_profile: "half-even".to_owned(),
- subtotal_mantissa: subtotal_mantissa.clone(),
- discount_total_mantissa: "0".to_owned(),
- adjustment_total_mantissa: "0".to_owned(),
- total_mantissa: subtotal_mantissa,
- adjustments: Vec::new(),
- },
- fulfillment: FulfillmentProfileV1 {
- profile_id: "market-pickup".to_owned(),
- method: "pickup".to_owned(),
- starts_at_unix_s: now + DEFAULT_FULFILLMENT_START_OFFSET_SECONDS,
- ends_at_unix_s: now
- + DEFAULT_FULFILLMENT_START_OFFSET_SECONDS
- + DEFAULT_FULFILLMENT_WINDOW_SECONDS,
- timezone: "UTC".to_owned(),
- utc_offset_seconds: 0,
- fold: 0,
- location_class: "seller_public_listing".to_owned(),
- requires_private_terms: false,
- },
- cancellation: TradeCancellationProfileV1 {
- profile_id: "buyer-pre-agreement".to_owned(),
- buyer_pre_agreement: true,
- post_agreement_cutoff_unix_s: None,
- },
- private_terms: None,
- proposal_expires_at_unix_s: now + DEFAULT_PROPOSAL_TTL_SECONDS,
- })
-}
-
-fn resolve_active_listing_state(
- config: &RuntimeConfig,
- listing_addr: &str,
- parsed: &ParsedListingAddress,
-) -> Result<ActiveListingState, RuntimeError> {
- let executor = SqlxSqliteExecutor::open(&config.local.replica_store_path)?;
- let key = format!(
- "{}:{}:{}",
- parsed.kind, parsed.seller_pubkey, parsed.listing_id
- );
- let state = nostr_event_head::find_one(
- &executor,
- &INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs {
- on: NostrEventHeadQueryBindValues::Key { key },
- }),
- )
- .map_err(|error| RuntimeError::Config(format!("resolve listing event state: {error:?}")))?
- .result
- .ok_or_else(|| {
- RuntimeError::Config(format!(
- "listing address `{listing_addr}` is missing latest listing event state; run `radroots market pull`"
- ))
- })?;
- MutationId::parse(state.content_hash.as_str()).map_err(|error| {
- RuntimeError::Config(format!(
- "listing content hash is not a 32-byte hex digest: {error}"
- ))
- })?;
- state
- .last_event_id
- .parse::<radroots_event::EventId>()
- .map_err(|error| {
- RuntimeError::Config(format!("listing latest event id is invalid: {error}"))
- })?;
- Ok(ActiveListingState {
- last_event_id: state.last_event_id,
- content_hash: state.content_hash,
- })
-}
-
-fn resolve_farm_id(config: &RuntimeConfig, seller_pubkey: &str) -> Result<DTag, RuntimeError> {
- let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?);
- let d_tag = db.farm_unique_d_tag_by_pubkey(seller_pubkey)?.ok_or_else(|| {
- RuntimeError::Config(format!(
- "seller `{seller_pubkey}` must have exactly one farm profile in the local replica before creating a trade proposal draft"
- ))
- })?;
- DTag::parse(d_tag.as_str())
- .map_err(|error| RuntimeError::Config(format!("farm d tag is invalid: {error}")))
-}
-
-fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, RuntimeError> {
- let parsed = ClassifiedListingAddress::parse(raw)
- .map_err(|error| RuntimeError::Config(format!("listing address is invalid: {error}")))?;
- let (kind, rest) = parsed
- .as_str()
- .split_once(':')
- .ok_or_else(|| RuntimeError::Config("listing address has invalid format".to_owned()))?;
- let (seller_pubkey, listing_id) = rest
- .split_once(':')
- .ok_or_else(|| RuntimeError::Config("listing address has invalid format".to_owned()))?;
- let kind = kind
- .parse::<u32>()
- .map_err(|_| RuntimeError::Config("listing address kind is invalid".to_owned()))?;
- Ok(ParsedListingAddress {
- kind,
- seller_pubkey: seller_pubkey.to_owned(),
- listing_id: listing_id.to_owned(),
- })
-}
-
-fn proposal_economics_view(envelope: &TradeMutationEnvelopeV1) -> TradeCandidateDraftEconomicsView {
- let TradeMutationBodyV1::Proposal { candidate } = &envelope.body else {
- unreachable!("proposal draft envelope is a proposal")
- };
- TradeCandidateDraftEconomicsView {
- currency_code: candidate.economics.currency_code.clone(),
- currency_exponent: u32::from(candidate.economics.currency_exponent),
- subtotal_mantissa: candidate.economics.subtotal_mantissa.clone(),
- discount_total_mantissa: candidate.economics.discount_total_mantissa.clone(),
- adjustment_total_mantissa: candidate.economics.adjustment_total_mantissa.clone(),
- total_mantissa: candidate.economics.total_mantissa.clone(),
- }
-}
-
-fn proposal_candidate_id(envelope: &TradeMutationEnvelopeV1) -> Option<String> {
- let TradeMutationBodyV1::Proposal { candidate } = &envelope.body else {
- return None;
- };
- candidate.candidate_id.as_ref().map(ToString::to_string)
-}
-
-fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsFilter {
- ITradeProductFieldsFilter {
- id: None,
- created_at: None,
- updated_at: None,
- key: None,
- category: None,
- title: None,
- summary: None,
- process: None,
- lot: None,
- profile: None,
- year: None,
- qty_amt: None,
- qty_amt_exact: None,
- qty_unit: None,
- qty_label: None,
- qty_avail: None,
- price_amt: None,
- price_amt_exact: None,
- price_currency: None,
- price_qty_amt: None,
- price_qty_amt_exact: None,
- price_qty_unit: None,
- listing_addr: Some(listing_addr.to_owned()),
- primary_bin_id: None,
- verified_primary_bin_id: None,
- notes: None,
- }
-}
-
-fn required(value: Option<String>, field: &str) -> Result<String, RuntimeError> {
- value
- .and_then(|value| non_empty_ref(value.as_str()).map(str::to_owned))
- .ok_or_else(|| RuntimeError::Config(format!("listing {field} is missing")))
-}
-
-fn verified_primary_bin(
- primary_bin_id: Option<String>,
- verified_primary_bin_id: Option<String>,
-) -> Result<String, RuntimeError> {
- let primary = required(primary_bin_id, "primary_bin_id")?;
- let verified = required(verified_primary_bin_id, "verified_primary_bin_id")?;
- if primary != verified {
- return Err(RuntimeError::Config(format!(
- "listing primary bin `{primary}` does not match verified primary bin `{verified}`"
- )));
- }
- Ok(primary)
-}
-
-fn validate_product_bin(product: &ProductFacts, bin_id: &str) -> Result<(), RuntimeError> {
- if product.primary_bin_id != bin_id {
- return Err(RuntimeError::Config(format!(
- "basket bin `{bin_id}` does not match listing primary bin `{}`",
- product.primary_bin_id
- )));
- }
- Ok(())
-}
-
-fn exact_positive_decimal(value: &str, field: &str) -> Result<Decimal, RuntimeError> {
- let parsed = value
- .trim()
- .parse::<Decimal>()
- .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))?;
- if parsed.is_zero() || parsed.is_sign_negative() {
- return Err(RuntimeError::Config(format!(
- "listing {field} must be greater than zero"
- )));
- }
- Ok(parsed)
-}
-
-fn decimal_mantissa_at_scale(mut value: Decimal, scale: u32) -> String {
- value.rescale(scale);
- value.to_string().replace('.', "")
-}
-
-fn candidate_draft_file(config: &RuntimeConfig, trade_id: &str) -> PathBuf {
- config
- .paths
- .app_data_root
- .join(TRADE_CANDIDATE_DRAFTS_DIR)
- .join(format!("{trade_id}.json"))
-}
-
-fn next_trade_id() -> Result<TradeId, RuntimeError> {
- let nanos = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|duration| duration.as_nanos())
- .unwrap_or_default();
- let counter = u128::from(TRADE_COUNTER.fetch_add(1, Ordering::Relaxed));
- let value = format!("{:032x}", nanos ^ counter);
- trade_id(value.as_str(), "trade_id")
-}
-
-fn now_unix() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|value| value.as_secs())
- .unwrap_or_default()
-}
-
-fn trade_id(value: &str, field: &str) -> Result<TradeId, RuntimeError> {
- TradeId::parse(value)
- .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}")))
-}
-
-fn trade_candidate_id(value: &str, field: &str) -> Result<CandidateId, RuntimeError> {
- CandidateId::parse(value)
- .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}")))
-}
-
-fn inventory_bin_id(value: &str, field: &str) -> Result<InventoryBinId, RuntimeError> {
- InventoryBinId::parse(value)
- .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}")))
-}
-
-fn pubkey(value: &str, field: &str) -> Result<PublicKey, RuntimeError> {
- PublicKey::from_hex(value)
- .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}")))
-}
-
-fn non_empty_ref(value: &str) -> Option<&str> {
- let value = value.trim();
- if value.is_empty() { None } else { Some(value) }
-}
diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs
@@ -1,532 +0,0 @@
-use radroots_transport::{
- RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity,
- RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportStatus,
-};
-use serde_json::Value as JsonValue;
-use std::fs;
-use toml::{Value, map::Map};
-
-use crate::ops::OperationData;
-use crate::runtime::RuntimeError;
-use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
-use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession};
-use crate::view::runtime::{
- TransportDeliveryInspectView, TransportDeliveryRetryView, TransportOperationCapabilitiesView,
- TransportProfileSummaryView, TransportProfileView, TransportRuntimeStatusView,
- TransportStatusInspectView,
-};
-
-const TRANSPORT_SOURCE: &str = "transport profile config";
-const RADROOTS_RETICULUM_ENDPOINT_URI: &str = "reticulum:local";
-const RADROOTS_RETICULUM_SCOPE_ID: &str = "local";
-const RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE: &str =
- "Reticulum transport is not available in this release";
-pub fn profile(config: &RuntimeConfig) -> TransportProfileView {
- active_profile_view(config)
-}
-
-pub fn set_profile(
- config: &RuntimeConfig,
- input: &OperationData,
-) -> Result<TransportProfileView, RuntimeError> {
- let kind = string_input(input, "kind").unwrap_or("local_only");
- let mut transport = Map::new();
- transport.insert("profile".to_owned(), Value::String(kind.to_owned()));
- match kind {
- "local_only" => {}
- "nostr" => {
- let relays = string_array_input(input, "nostr_relays");
- if relays.is_empty() {
- return Err(RuntimeError::Config(
- "transport profile `nostr` requires at least one --nostr-relay".to_owned(),
- ));
- }
- let mut nostr = Map::new();
- nostr.insert(
- "relay_urls".to_owned(),
- Value::Array(relays.into_iter().map(Value::String).collect()),
- );
- transport.insert("nostr".to_owned(), Value::Table(nostr));
- }
- "reticulum" => {
- let behavior =
- string_input(input, "reticulum_behavior").unwrap_or("reject_delivery_attempts");
- let scope =
- string_input(input, "reticulum_scope").unwrap_or(RADROOTS_RETICULUM_SCOPE_ID);
- let mut reticulum = Map::new();
- reticulum.insert("behavior".to_owned(), Value::String(behavior.to_owned()));
- reticulum.insert("scope".to_owned(), Value::String(scope.to_owned()));
- if let Some(agent_endpoint) = string_input(input, "reticulum_agent_endpoint") {
- reticulum.insert(
- "agent_endpoint".to_owned(),
- Value::String(agent_endpoint.to_owned()),
- );
- }
- transport.insert("reticulum".to_owned(), Value::Table(reticulum));
- }
- "multi_target" => {
- let relays = string_array_input(input, "nostr_relays");
- if relays.is_empty() {
- return Err(RuntimeError::Config(
- "transport profile `multi_target` requires at least one --nostr-relay"
- .to_owned(),
- ));
- }
- let behavior =
- string_input(input, "reticulum_behavior").unwrap_or("reject_delivery_attempts");
- let scope =
- string_input(input, "reticulum_scope").unwrap_or(RADROOTS_RETICULUM_SCOPE_ID);
- let mut nostr = Map::new();
- nostr.insert(
- "relay_urls".to_owned(),
- Value::Array(relays.into_iter().map(Value::String).collect()),
- );
- let mut reticulum = Map::new();
- reticulum.insert("behavior".to_owned(), Value::String(behavior.to_owned()));
- reticulum.insert("scope".to_owned(), Value::String(scope.to_owned()));
- if let Some(agent_endpoint) = string_input(input, "reticulum_agent_endpoint") {
- reticulum.insert(
- "agent_endpoint".to_owned(),
- Value::String(agent_endpoint.to_owned()),
- );
- }
- transport.insert("nostr".to_owned(), Value::Table(nostr));
- transport.insert("reticulum".to_owned(), Value::Table(reticulum));
- }
- other => {
- return Err(RuntimeError::Config(format!(
- "transport profile kind `{other}` is not supported"
- )));
- }
- }
- update_app_config_table(config, "transport", Value::Table(transport))?;
- Ok(profile_view_from_parts(
- kind,
- string_array_input(input, "nostr_relays"),
- string_input(input, "reticulum_behavior").map(str::to_owned),
- reticulum_scope_for_profile(kind, input),
- string_input(input, "reticulum_agent_endpoint").map(str::to_owned),
- "configured",
- ))
-}
-
-pub fn status(config: &RuntimeConfig) -> TransportStatusInspectView {
- let profile = active_profile_view(config);
-
- TransportStatusInspectView {
- state: "ready".to_owned(),
- source: TRANSPORT_SOURCE.to_owned(),
- active_profile: profile.summary(),
- transports: profile.transport_statuses,
- }
-}
-
-pub fn outbox_status(
- config: &RuntimeConfig,
-) -> Result<TransportDeliveryInspectView, CliSdkAdapterError> {
- let profile = active_profile_view(config);
- let session = if profile.profile_delivery_usable {
- CliSdkSession::connect(config)?
- } else {
- CliSdkSession::connect_storage_status(config)?
- };
- let receipt = crate::runtime::sync::canonical_status(&session)?;
- let outbox = receipt.outbox();
- let state = if profile.configured_state == "configured" {
- "ready".to_owned()
- } else {
- profile.configured_state.clone()
- };
- Ok(TransportDeliveryInspectView {
- state,
- source: "SDK transport outbox".to_owned(),
- transport_profile: profile.profile_id,
- total_count: i64::try_from(outbox.total().unwrap_or_default()).unwrap_or(i64::MAX),
- pending_count: i64::try_from(outbox.pending).unwrap_or(i64::MAX),
- retryable_count: i64::try_from(outbox.retryable).unwrap_or(i64::MAX),
- terminal_count: i64::try_from(outbox.satisfied + outbox.exhausted).unwrap_or(i64::MAX),
- deferred_until_implemented_count: 0,
- ready_signed_count: i64::try_from(outbox.pending + outbox.retryable).unwrap_or(i64::MAX),
- publishing_count: i64::try_from(outbox.leased).unwrap_or(i64::MAX),
- last_attempt_at_ms: None,
- last_error: None,
- actions: vec!["radroots transport outbox push".to_owned()],
- })
-}
-
-pub fn outbox_push(
- config: &RuntimeConfig,
-) -> Result<TransportDeliveryRetryView, CliSdkAdapterError> {
- if config.output.dry_run {
- let status = outbox_status(config)?;
- return Ok(TransportDeliveryRetryView {
- state: "dry_run".to_owned(),
- source: "SDK transport outbox".to_owned(),
- attempted_events: 0,
- published_events: 0,
- retryable_events: 0,
- terminal_events: 0,
- target_count: usize::try_from(status.ready_signed_count).unwrap_or_default(),
- reason: Some("dry run requested; transport outbox push skipped".to_owned()),
- actions: vec!["radroots transport outbox status".to_owned()],
- });
- }
- let session = CliSdkSession::connect(config)?;
- let receipt = crate::runtime::sync::deliver_pending(&session)?;
- let attempted_events = receipt.outcomes().len();
- let published_events = receipt.succeeded();
- let failed_count = receipt.failed();
- let state = if attempted_events == 0 {
- "ready"
- } else if failed_count == 0 {
- "published"
- } else if published_events > 0 {
- "partial"
- } else {
- "unavailable"
- };
- Ok(TransportDeliveryRetryView {
- state: state.to_owned(),
- source: "SDK transport outbox".to_owned(),
- attempted_events,
- published_events,
- retryable_events: failed_count,
- terminal_events: 0,
- target_count: config.transport.nostr_relay_urls.len(),
- reason: if attempted_events == 0 {
- Some("canonical outbox had no ready delivery plans".to_owned())
- } else if failed_count > 0 {
- Some(format!(
- "{failed_count} canonical delivery outcome(s) failed"
- ))
- } else {
- None
- },
- actions: vec!["radroots transport outbox status".to_owned()],
- })
-}
-
-fn active_profile_view(config: &RuntimeConfig) -> TransportProfileView {
- match config.transport.profile {
- TransportProfileKind::LocalOnly => profile_view_from_parts(
- "local_only",
- Vec::new(),
- None,
- None,
- None,
- "configured",
- ),
- TransportProfileKind::Nostr => profile_view_from_parts(
- "nostr",
- config.transport.nostr_relay_urls.clone(),
- None,
- None,
- None,
- if config.transport.nostr_relay_urls.is_empty() {
- "unconfigured"
- } else {
- "configured"
- },
- ),
- TransportProfileKind::Reticulum => profile_view_from_parts(
- "reticulum",
- Vec::new(),
- Some(config.transport.reticulum_behavior.as_str().to_owned()),
- Some(config.transport.reticulum_scope.clone()),
- config.transport.reticulum_agent_endpoint.clone(),
- "configured",
- ),
- TransportProfileKind::MultiTarget => profile_view_from_parts(
- "multi_target",
- config.transport.nostr_relay_urls.clone(),
- Some(config.transport.reticulum_behavior.as_str().to_owned()),
- Some(config.transport.reticulum_scope.clone()),
- config.transport.reticulum_agent_endpoint.clone(),
- if config.transport.nostr_relay_urls.is_empty() {
- "unconfigured"
- } else {
- "configured"
- },
- )
- .with_message(
- "Multi-target transport publishes through configured Nostr relays and reports Reticulum availability"
- .to_owned(),
- ),
- }
-}
-
-fn profile_view_from_parts(
- profile_id: &str,
- nostr_relays: Vec<String>,
- reticulum_behavior: Option<String>,
- reticulum_scope: Option<String>,
- reticulum_agent_endpoint: Option<String>,
- configured_state: &str,
-) -> TransportProfileView {
- let transport_statuses = transport_statuses_from_parts(profile_id, nostr_relays.as_slice());
- let profile_delivery_usable = transport_statuses
- .iter()
- .any(|status| status.usable_for_delivery);
- let message = match profile_id {
- "nostr" if profile_delivery_usable => "Nostr relay transport is configured for delivery",
- "nostr" => "Nostr transport requires configured Nostr relay targets",
- "reticulum" => RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
- "multi_target" if profile_delivery_usable => {
- "Multi-target transport publishes through configured Nostr relays and reports Reticulum availability"
- }
- "multi_target" => "Multi-target transport requires configured Nostr relay targets",
- _ => "Local-only profile does not deliver to network transports",
- };
- TransportProfileView {
- state: configured_state.to_owned(),
- source: TRANSPORT_SOURCE.to_owned(),
- profile_id: profile_id.to_owned(),
- profile_kind: profile_id.to_owned(),
- configured_state: configured_state.to_owned(),
- profile_delivery_usable,
- message: message.to_owned(),
- nostr_relays,
- reticulum_behavior,
- reticulum_scope,
- reticulum_agent_endpoint,
- transport_statuses,
- actions: profile_actions(profile_id, profile_delivery_usable),
- }
-}
-
-fn transport_statuses_from_parts(
- profile_id: &str,
- nostr_relays: &[String],
-) -> Vec<TransportRuntimeStatusView> {
- match profile_id {
- "nostr" => vec![transport_runtime_status_view(nostr_transport_status(
- profile_id,
- !nostr_relays.is_empty(),
- ))],
- "reticulum" => {
- vec![transport_runtime_status_view(reticulum_transport_status(
- profile_id,
- ))]
- }
- "multi_target" => vec![
- transport_runtime_status_view(nostr_transport_status(
- profile_id,
- !nostr_relays.is_empty(),
- )),
- transport_runtime_status_view(reticulum_transport_status(profile_id)),
- ],
- _ => vec![transport_runtime_status_view(local_transport_status(
- profile_id,
- ))],
- }
-}
-
-fn local_transport_status(profile_id: &str) -> RadrootsTransportStatus {
- RadrootsTransportStatus::new(
- RadrootsTransportKind::Local,
- true,
- RadrootsTransportImplementationState::Real,
- false,
- "Local-only profile writes only to local state",
- )
- .with_profile_id(profile_id)
-}
-
-fn nostr_transport_status(profile_id: &str, targets_configured: bool) -> RadrootsTransportStatus {
- RadrootsTransportStatus::new(
- RadrootsTransportKind::Nostr,
- targets_configured,
- RadrootsTransportImplementationState::Real,
- targets_configured,
- if targets_configured {
- "Nostr relay transport is configured for delivery"
- } else {
- "Nostr transport requires configured Nostr relay targets"
- },
- )
- .with_profile_id(profile_id)
-}
-
-fn reticulum_transport_status(profile_id: &str) -> RadrootsTransportStatus {
- RadrootsTransportStatus::new(
- RadrootsTransportKind::Reticulum,
- true,
- RadrootsTransportImplementationState::Real,
- false,
- RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
- )
- .with_profile_id(profile_id)
- .with_endpoint_uri(RADROOTS_RETICULUM_ENDPOINT_URI)
- .with_maturity(RadrootsTransportCapabilityMaturity::Preview)
- .with_availability(RadrootsTransportCapabilityAvailability::Unavailable)
-}
-
-fn transport_runtime_status_view(status: RadrootsTransportStatus) -> TransportRuntimeStatusView {
- TransportRuntimeStatusView {
- transport: status.kind.canonical_label(),
- profile_id: status.profile_id,
- endpoint_uri: status.endpoint_uri,
- configured: status.configured,
- implementation: transport_implementation_label(status.implementation).to_owned(),
- maturity: transport_maturity_label(status.maturity).to_owned(),
- availability: transport_availability_label(status.availability).to_owned(),
- usable_for_delivery: status.usable_for_delivery,
- capabilities: TransportOperationCapabilitiesView {
- deliver: status.capabilities.deliver,
- fetch: status.capabilities.fetch,
- },
- message: status.message,
- }
-}
-
-fn transport_implementation_label(state: RadrootsTransportImplementationState) -> &'static str {
- match state {
- RadrootsTransportImplementationState::Real => "real",
- RadrootsTransportImplementationState::Mock => "mock",
- }
-}
-
-fn transport_maturity_label(maturity: RadrootsTransportCapabilityMaturity) -> &'static str {
- match maturity {
- RadrootsTransportCapabilityMaturity::Experimental => "experimental",
- RadrootsTransportCapabilityMaturity::Preview => "preview",
- RadrootsTransportCapabilityMaturity::Stable => "stable",
- }
-}
-
-fn transport_availability_label(
- availability: RadrootsTransportCapabilityAvailability,
-) -> &'static str {
- match availability {
- RadrootsTransportCapabilityAvailability::Available => "available",
- RadrootsTransportCapabilityAvailability::Degraded => "degraded",
- RadrootsTransportCapabilityAvailability::Unavailable => "unavailable",
- }
-}
-
-fn reticulum_scope_for_profile(profile_id: &str, input: &OperationData) -> Option<String> {
- match profile_id {
- "reticulum" | "multi_target" => Some(
- string_input(input, "reticulum_scope")
- .unwrap_or(RADROOTS_RETICULUM_SCOPE_ID)
- .to_owned(),
- ),
- _ => None,
- }
-}
-
-fn profile_actions(profile_id: &str, profile_delivery_usable: bool) -> Vec<String> {
- if profile_delivery_usable {
- return Vec::new();
- }
- match profile_id {
- "reticulum" => vec![
- "radroots transport status inspect".to_owned(),
- "radroots transport config inspect".to_owned(),
- ],
- "nostr" => {
- vec![
- "radroots transport config update --kind nostr --nostr-relay wss://relay.example.com"
- .to_owned(),
- ]
- }
- "multi_target" => {
- vec![
- "radroots transport config update --kind multi-target --nostr-relay wss://relay.example.com"
- .to_owned(),
- ]
- }
- _ => vec!["radroots transport config inspect".to_owned()],
- }
-}
-
-trait TransportProfileViewMessage {
- fn with_message(self, message: String) -> Self;
- fn summary(&self) -> TransportProfileSummaryView;
-}
-
-impl TransportProfileViewMessage for TransportProfileView {
- fn with_message(mut self, message: String) -> Self {
- self.message = message;
- self
- }
-
- fn summary(&self) -> TransportProfileSummaryView {
- TransportProfileSummaryView {
- profile_id: self.profile_id.clone(),
- profile_kind: self.profile_kind.clone(),
- configured_state: self.configured_state.clone(),
- profile_delivery_usable: self.profile_delivery_usable,
- message: self.message.clone(),
- }
- }
-}
-
-pub(crate) fn update_app_config_table(
- config: &RuntimeConfig,
- key: &str,
- value: Value,
-) -> Result<(), RuntimeError> {
- let path = &config.paths.app_config_path;
- let mut document = if path.exists() {
- let raw = fs::read_to_string(path)?;
- toml::from_str::<Value>(&raw)
- .map_err(|error| RuntimeError::Config(format!("failed to parse app config: {error}")))?
- } else {
- Value::Table(Map::new())
- };
- let Some(table) = document.as_table_mut() else {
- return Err(RuntimeError::Config(
- "app config root must be a TOML table".to_owned(),
- ));
- };
- table.remove("publish");
- table.remove("relays");
- table.insert(key.to_owned(), value);
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent)?;
- }
- let rendered = toml::to_string_pretty(&document)
- .map_err(|error| RuntimeError::Config(format!("failed to render app config: {error}")))?;
- fs::write(path, rendered)?;
- Ok(())
-}
-
-fn string_input<'a>(input: &'a OperationData, key: &str) -> Option<&'a str> {
- input.get(key).and_then(JsonValue::as_str)
-}
-
-fn string_array_input(input: &OperationData, key: &str) -> Vec<String> {
- input
- .get(key)
- .and_then(JsonValue::as_array)
- .into_iter()
- .flatten()
- .filter_map(JsonValue::as_str)
- .map(str::to_owned)
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn reticulum_profile_remains_explicitly_unavailable() {
- let status = reticulum_transport_status("reticulum");
- assert!(!status.usable_for_delivery);
- assert_eq!(
- status.endpoint_uri.as_deref(),
- Some(RADROOTS_RETICULUM_ENDPOINT_URI)
- );
- assert_eq!(status.message, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
- }
-
- #[test]
- fn experimental_maturity_has_a_stable_terminal_label() {
- assert_eq!(
- transport_maturity_label(RadrootsTransportCapabilityMaturity::Experimental),
- "experimental"
- );
- }
-}
diff --git a/src/view/mod.rs b/src/view/mod.rs
@@ -1 +0,0 @@
-pub mod runtime;
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -1,2800 +0,0 @@
-use std::process::ExitCode;
-
-use radroots_event::farm::Farm;
-use radroots_event::id::ClassifiedListingAddress;
-use radroots_event::listing::operational::OperationalListingPublicLocation;
-use radroots_identity::account::Record as AccountRecord;
-use serde::Serialize;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CommandDisposition {
- Success,
- NotFound,
- ValidationFailed,
- Unconfigured,
- ExternalUnavailable,
- Unsupported,
- InternalError,
-}
-
-impl CommandDisposition {
- pub fn exit_code(self) -> ExitCode {
- match self {
- Self::Success => ExitCode::SUCCESS,
- Self::NotFound => ExitCode::from(4),
- Self::ValidationFailed => ExitCode::from(10),
- Self::Unconfigured => ExitCode::from(3),
- Self::ExternalUnavailable => ExitCode::from(4),
- Self::Unsupported => ExitCode::from(5),
- Self::InternalError => ExitCode::from(1),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ConfigShowView {
- pub source: String,
- pub output: OutputRuntimeView,
- pub interaction: InteractionRuntimeView,
- pub config_files: ConfigFilesRuntimeView,
- pub paths: PathsRuntimeView,
- pub logging: LoggingRuntimeView,
- pub account: AccountRuntimeView,
- pub signer: SignerRuntimeView,
- pub publish: PublishRuntimeView,
- pub relay: RelayRuntimeView,
- pub local: LocalRuntimeView,
- pub myc: MycRuntimeView,
- pub write_plane: WritePlaneRuntimeView,
- pub workflow: WorkflowRuntimeView,
- pub hyf_provider: HyfProviderRuntimeView,
- pub hyf: HyfRuntimeView,
- pub rpc: RpcRuntimeView,
- pub capability_bindings: Vec<CapabilityBindingRuntimeView>,
- pub resolved_providers: Vec<ResolvedProviderRuntimeView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeActionView {
- pub action: String,
- pub runtime_id: String,
- pub instance_id: String,
- pub instance_source: String,
- pub runtime_group: String,
- pub state: String,
- pub source: String,
- pub detail: String,
- pub mutates_bindings: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub next_step: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeManagedConfigView {
- pub runtime_id: String,
- pub instance_id: String,
- pub instance_source: String,
- pub runtime_group: String,
- pub state: String,
- pub source: String,
- pub detail: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config_format: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config_path: Option<String>,
- pub config_present: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requires_bootstrap_secret: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requires_config_bootstrap: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub requires_signer_provider: Option<bool>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeLogsView {
- pub runtime_id: String,
- pub instance_id: String,
- pub instance_source: String,
- pub runtime_group: String,
- pub state: String,
- pub source: String,
- pub detail: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub stdout_log_path: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub stderr_log_path: Option<String>,
- pub stdout_log_present: bool,
- pub stderr_log_present: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeStatusView {
- pub runtime_id: String,
- pub instance_id: String,
- pub instance_source: String,
- pub runtime_group: String,
- pub management_posture: String,
- pub state: String,
- pub source: String,
- pub detail: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub management_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub service_manager_integration: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub uses_absolute_binary_paths: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub preferred_cli_binding: Option<bool>,
- pub install_state: String,
- pub health_state: String,
- pub health_source: String,
- pub registry_path: String,
- pub lifecycle_actions: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub instance_paths: Option<RuntimeInstancePathsView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub instance_record: Option<RuntimeInstanceRecordView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeInstancePathsView {
- pub install_dir: String,
- pub state_dir: String,
- pub logs_dir: String,
- pub run_dir: String,
- pub secrets_dir: String,
- pub pid_file_path: String,
- pub stdout_log_path: String,
- pub stderr_log_path: String,
- pub metadata_path: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RuntimeInstanceRecordView {
- pub management_mode: String,
- pub install_state: String,
- pub binary_path: String,
- pub config_path: String,
- pub logs_path: String,
- pub run_path: String,
- pub installed_version: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub health_endpoint: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub secret_material_ref: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_started_at: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_stopped_at: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub notes: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct OutputRuntimeView {
- pub format: String,
- pub verbosity: String,
- pub dry_run: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct InteractionRuntimeView {
- pub input_enabled: bool,
- pub assume_yes: bool,
- pub stdin_tty: bool,
- pub stdout_tty: bool,
- pub prompts_allowed: bool,
- pub confirmations_allowed: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ConfigFilesRuntimeView {
- pub user_present: bool,
- pub workspace_present: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LoggingRuntimeView {
- pub initialized: bool,
- pub filter: String,
- pub stdout: bool,
- pub directory: Option<String>,
- pub current_file: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct PathsRuntimeView {
- pub profile: String,
- pub profile_source: String,
- pub allowed_profiles: Vec<String>,
- pub root_source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub repo_local_root: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub repo_local_root_source: Option<String>,
- pub subordinate_path_override_source: String,
- pub app_namespace: String,
- pub shared_accounts_namespace: String,
- pub shared_identities_namespace: String,
- pub app_config_path: String,
- pub workspace_config_enabled: bool,
- pub workspace_config_path: Option<String>,
- pub app_data_root: String,
- pub shared_cache_root: String,
- pub app_logs_root: String,
- pub shared_accounts_data_root: String,
- pub shared_accounts_secrets_root: String,
- pub default_identity_path: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountRuntimeView {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub selector: Option<String>,
- pub store_path: String,
- pub secrets_dir: String,
- pub identity_path: String,
- pub secret_backend: AccountSecretRuntimeView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountSecretRuntimeView {
- pub contract_default_backend: String,
- pub allowed_backends: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub host_vault_policy: Option<String>,
- pub uses_protected_store: bool,
- pub configured_primary: String,
- pub state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub active_backend: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SignerRuntimeView {
- pub mode: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RelayRuntimeView {
- pub count: usize,
- pub urls: Vec<String>,
- pub publish_policy: String,
- pub source: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct PublishRuntimeView {
- pub transport: String,
- pub source: String,
- pub transport_family: String,
- pub state: String,
- pub executable: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub signed_write_required: bool,
- pub relay: PublishRelayRuntimeView,
- pub provider: PublishProviderRuntimeView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct PublishRelayRuntimeView {
- pub ready: bool,
- pub count: usize,
- pub source: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct PublishProviderRuntimeView {
- pub provider_runtime_id: String,
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalRuntimeView {
- pub root: String,
- pub replica_store_path: String,
- pub backups_dir: String,
- pub exports_dir: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MycRuntimeView {
- pub executable: String,
- pub status_timeout_ms: u64,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct WorkflowRuntimeView {
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
- pub hyf_helper_state: String,
- pub hyf_helper_detail: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct HyfRuntimeView {
- pub enabled: bool,
- pub executable: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct HyfProviderRuntimeView {
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
- pub executable: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub protocol_version: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub deterministic_available: Option<bool>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct WritePlaneRuntimeView {
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
- pub detail: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RpcRuntimeView {
- pub url: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ResolvedProviderRuntimeView {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub provenance: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct CapabilityBindingRuntimeView {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub managed_account_ref: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_ref: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct DoctorView {
- pub ok: bool,
- pub state: String,
- pub account_resolution: AccountResolutionView,
- pub checks: Vec<DoctorCheckView>,
- pub source: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct DoctorCheckView {
- pub name: String,
- pub status: String,
- pub detail: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct IdentityPublicView {
- pub id: String,
- pub public_key_hex: String,
- pub public_key_npub: String,
-}
-
-impl IdentityPublicView {
- pub fn from_public_identity(identity: &radroots_identity::PublicIdentity) -> Self {
- Self {
- id: identity.id().to_string(),
- public_key_hex: identity.public_key().to_hex(),
- public_key_npub: radroots_nostr::key::public_key_to_npub(identity.public_key())
- .unwrap_or_else(|_| identity.public_key().to_hex()),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountSummaryView {
- pub id: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub display_name: Option<String>,
- pub signer: String,
- pub custody: String,
- pub write_capable: bool,
- pub is_default: bool,
-}
-
-impl AccountSummaryView {
- pub fn from_account_runtime(
- record: &AccountRecord,
- signer: &str,
- custody: &str,
- write_capable: bool,
- is_default: bool,
- ) -> Self {
- Self {
- id: record.id().to_string(),
- display_name: record.label().map(ToOwned::to_owned),
- signer: signer.to_owned(),
- custody: custody.to_owned(),
- write_capable,
- is_default,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountResolutionView {
- pub status: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub resolved_account: Option<AccountSummaryView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub default_account: Option<AccountSummaryView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountWhoamiView {
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub account_resolution: AccountResolutionView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub public_identity: Option<IdentityPublicView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl AccountWhoamiView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountNewView {
- pub state: String,
- pub source: String,
- pub account: AccountSummaryView,
- pub public_identity: IdentityPublicView,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountImportView {
- pub state: String,
- pub source: String,
- pub account: AccountSummaryView,
- pub public_identity: IdentityPublicView,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountUseView {
- pub state: String,
- pub source: String,
- pub default_account_id: String,
- pub account: AccountSummaryView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountClearDefaultView {
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cleared_account: Option<AccountSummaryView>,
- pub remaining_account_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountRemoveView {
- pub state: String,
- pub source: String,
- pub removed_account: AccountSummaryView,
- pub default_cleared: bool,
- pub remaining_account_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct AccountListView {
- pub source: String,
- pub count: usize,
- pub accounts: Vec<AccountSummaryView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalInitView {
- pub state: String,
- pub source: String,
- pub local_root: String,
- pub replica_store: String,
- pub path: String,
- pub replica_store_version: String,
- pub backup_format_version: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalStatusView {
- pub state: String,
- pub source: String,
- pub local_root: String,
- pub canonical_store: String,
- pub sdk_storage: String,
- pub sdk_root: String,
- pub sdk_existed_before_open: bool,
- pub storage: SdkStorageStatusView,
- pub integrity: SdkIntegrityView,
- pub derived_projection: LocalDerivedProjectionStatusView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl LocalStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "needs_attention" => CommandDisposition::ValidationFailed,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SdkStorageStatusView {
- pub backend: String,
- pub open_mode: String,
- pub writer_policy: String,
- pub shutdown: String,
- pub wal_enabled: bool,
- pub busy_timeout_ms: u32,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SdkIntegrityView {
- pub health: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub checked_at_unix_ms: Option<u64>,
- pub verified_members: u32,
- pub failed_members: u32,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalDerivedProjectionStatusView {
- pub state: String,
- pub source: String,
- pub replica_store: String,
- pub path: String,
- pub replica_store_version: String,
- pub backup_format_version: String,
- pub schema_hash: String,
- pub counts: LocalReplicaCountsView,
- pub sync: LocalReplicaSyncView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalReplicaCountsView {
- pub farms: u64,
- pub listings: u64,
- pub profiles: u64,
- pub relays: u64,
- pub event_states: u64,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalReplicaSyncView {
- pub expected_count: usize,
- pub pending_count: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SetupView {
- pub state: String,
- pub source: String,
- pub role: String,
- pub account_resolution: AccountResolutionView,
- pub local_state: String,
- pub local_root: String,
- pub relay_state: String,
- pub relay_count: usize,
- pub farm_state: String,
- #[serde(default)]
- pub ready: Vec<String>,
- #[serde(default)]
- pub needs_attention: Vec<String>,
- #[serde(default)]
- pub next: Vec<String>,
-}
-
-impl SetupView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct StatusView {
- pub state: String,
- pub source: String,
- pub account_resolution: AccountResolutionView,
- pub local_state: String,
- pub local_root: String,
- pub relay_state: String,
- pub relay_count: usize,
- pub farm_state: String,
- #[serde(default)]
- pub ready: Vec<String>,
- #[serde(default)]
- pub needs_attention: Vec<String>,
- #[serde(default)]
- pub next: Vec<String>,
-}
-
-impl StatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmSetupView {
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config: Option<FarmConfigSummaryView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmSetupView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmSetView {
- pub state: String,
- pub source: String,
- pub field: String,
- pub value: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config: Option<FarmConfigSummaryView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmSetView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPrivateLocationView {
- pub state: String,
- pub source: String,
- pub farm_addr: Option<String>,
- pub farm_d_tag: Option<String>,
- pub seller_account_id: Option<String>,
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub label: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub exact_location: Option<FarmPrivateExactLocationView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub public_locality: Option<FarmPrivatePublicLocalityView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub geonames_feature_id: Option<i64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub geonames_country_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub geonames_database_path: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cleared: Option<bool>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub candidates: Vec<FarmPrivateLocationCandidateView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPrivateExactLocationView {
- pub lat: f64,
- pub lng: f64,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPrivatePublicLocalityView {
- pub primary: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub city: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub region: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub country: Option<String>,
- pub geohash5: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPrivateLocationCandidateView {
- pub geonames_feature_id: i64,
- pub geonames_country_id: String,
- pub name: String,
- pub display_name: String,
- pub exact_location: FarmPrivateExactLocationView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub region: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub country: Option<String>,
-}
-
-impl FarmPrivateLocationView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "missing" => CommandDisposition::NotFound,
- "no_match" => CommandDisposition::NotFound,
- "ambiguous" => CommandDisposition::ValidationFailed,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmRebindView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub path: String,
- pub config_present: bool,
- pub dry_run: bool,
- pub seller_actor_source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub to_seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub to_seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey_changed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub publication_state_action: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config: Option<FarmConfigSummaryView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmRebindView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmStatusView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub path: String,
- pub config_present: bool,
- pub config_valid: bool,
- pub account_state: String,
- pub listing_defaults_state: String,
- pub transport_profile: String,
- pub publish_state: String,
- pub publish_executable: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub publish_reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub config: Option<FarmConfigSummaryView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub missing: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmGetView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub path: String,
- pub config_present: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub document: Option<FarmConfigDocumentView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmGetView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" | "missing" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublishView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub path: String,
- pub config_present: bool,
- pub dry_run: bool,
- pub seller_account_id: String,
- pub seller_pubkey: String,
- pub seller_actor_source: String,
- pub farm_d_tag: String,
- pub profile: FarmPublishComponentView,
- pub farm: FarmPublishComponentView,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub local_replica: Vec<FarmPublishLocalReplicaView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub missing: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FarmPublishView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "partial" | "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublishComponentView {
- pub state: String,
- pub rpc_method: String,
- pub event_kind: u32,
- pub deduplicated: bool,
- #[serde(default)]
- pub target_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub attempted_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub accepted_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub failed_transport_targets: Vec<TransportTargetFailureView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_status: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub idempotency_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job: Option<FarmPublishJobView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event: Option<FarmPublishEventView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublishLocalReplicaView {
- pub component: String,
- pub state: String,
- pub store_state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ingest_outcome: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublishJobView {
- pub rpc_method: String,
- pub state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub idempotency_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_mode: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublishEventView {
- pub kind: u32,
- pub author: String,
- pub content: String,
- pub tags: Vec<Vec<String>>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmConfigSummaryView {
- pub scope: String,
- pub path: String,
- pub seller_account_id: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- pub seller_actor_source: String,
- pub farm_d_tag: String,
- pub name: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- pub delivery_method: String,
- pub publication: FarmPublicationView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmConfigDocumentView {
- pub selection: FarmSelectionView,
- pub profile: FarmProfileDraftView,
- pub farm: Farm,
- pub listing_defaults: FarmListingDefaultsView,
- pub publication: FarmPublicationView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmProfileDraftView {
- pub name: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub display_name: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub nip05: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub about: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub website: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub picture: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub banner: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub lud06: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub lud16: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub bot: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmSelectionView {
- pub scope: String,
- pub seller_account_id: String,
- pub farm_d_tag: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmListingDefaultsView {
- pub delivery_method: String,
- pub location: OperationalListingPublicLocation,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FarmPublicationView {
- pub profile_state: String,
- pub farm_state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub profile_event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub profile_published_at: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_published_at: Option<u64>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindView {
- pub state: String,
- pub source: String,
- pub query: String,
- pub count: usize,
- pub relay_count: usize,
- pub replica_store: String,
- pub freshness: SyncFreshnessView,
- pub results: Vec<FindResultView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub hyf: Option<FindHyfView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl FindView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindHyfView {
- pub state: String,
- pub source: String,
- pub rewritten_query: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub query_terms: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MarketReadinessView {
- pub protocol_valid: bool,
- pub marketplace_eligible: bool,
- pub checkout_enabled: bool,
- pub primary_bin_verified: bool,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub reason_codes: Vec<String>,
-}
-
-impl MarketReadinessView {
- pub fn unavailable(reason_code: impl Into<String>) -> Self {
- Self {
- protocol_valid: false,
- marketplace_eligible: false,
- checkout_enabled: false,
- primary_bin_verified: false,
- reason_codes: vec![reason_code.into()],
- }
- }
-
- #[expect(
- clippy::too_many_arguments,
- reason = "market candidate construction maps projection fields explicitly"
- )]
- pub fn from_market_projection(
- listing_addr: Option<&str>,
- primary_bin_id: Option<&str>,
- verified_primary_bin_id: Option<&str>,
- title: Option<&str>,
- category: Option<&str>,
- available_amount: Option<i64>,
- price_amount: f64,
- price_currency: &str,
- price_per_amount: f64,
- ) -> Self {
- let protocol_valid = listing_addr
- .is_some_and(|listing_addr| ClassifiedListingAddress::parse(listing_addr).is_ok());
- let marketplace_eligible = protocol_valid
- && title.is_some_and(|title| !title.trim().is_empty())
- && category.is_some_and(|category| !category.trim().is_empty());
- let inventory_available = available_amount.is_some_and(|amount| amount > 0);
- let primary_bin_available =
- primary_bin_id.is_some_and(|primary_bin_id| !primary_bin_id.trim().is_empty());
- let primary_bin_verified = protocol_valid
- && primary_bin_available
- && primary_bin_id.is_some_and(|primary_bin_id| {
- verified_primary_bin_id.is_some_and(|verified_primary_bin_id| {
- verified_primary_bin_id.trim() == primary_bin_id.trim()
- })
- });
- let price_available = price_amount.is_finite()
- && price_amount > 0.0
- && !price_currency.trim().is_empty()
- && price_per_amount.is_finite()
- && price_per_amount > 0.0;
- let checkout_enabled =
- marketplace_eligible && inventory_available && primary_bin_verified && price_available;
- let mut reason_codes = Vec::new();
- if !protocol_valid {
- reason_codes.push("listing_protocol_invalid".to_owned());
- }
- if protocol_valid && !marketplace_eligible {
- reason_codes.push("listing_marketplace_ineligible".to_owned());
- }
- if marketplace_eligible && !checkout_enabled {
- reason_codes.push("listing_checkout_disabled".to_owned());
- if !inventory_available {
- reason_codes.push("listing_inventory_unavailable".to_owned());
- }
- if !primary_bin_available {
- reason_codes.push("listing_primary_bin_missing".to_owned());
- } else if !primary_bin_verified {
- reason_codes.push("listing_primary_bin_invalid".to_owned());
- }
- if !price_available {
- reason_codes.push("listing_price_unavailable".to_owned());
- }
- }
- Self {
- protocol_valid,
- marketplace_eligible,
- checkout_enabled,
- primary_bin_verified,
- reason_codes,
- }
- }
-}
-
-#[cfg(test)]
-mod market_readiness_tests {
- use super::MarketReadinessView;
-
- const LISTING_ADDR: &str = "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAg";
-
- #[test]
- fn market_readiness_separates_protocol_marketplace_and_order_request_state() {
- let enabled = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- Some("bin-1"),
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(enabled.protocol_valid);
- assert!(enabled.marketplace_eligible);
- assert!(enabled.checkout_enabled);
- assert!(enabled.primary_bin_verified);
- assert!(enabled.reason_codes.is_empty());
-
- let invalid = MarketReadinessView::from_market_projection(
- None,
- Some("bin-1"),
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(!invalid.protocol_valid);
- assert!(!invalid.marketplace_eligible);
- assert!(!invalid.checkout_enabled);
- assert!(!invalid.primary_bin_verified);
- assert_eq!(invalid.reason_codes, vec!["listing_protocol_invalid"]);
-
- let ineligible = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- Some("bin-1"),
- Some("bin-1"),
- Some(" "),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(ineligible.protocol_valid);
- assert!(!ineligible.marketplace_eligible);
- assert!(!ineligible.checkout_enabled);
- assert!(ineligible.primary_bin_verified);
- assert_eq!(
- ineligible.reason_codes,
- vec!["listing_marketplace_ineligible"]
- );
-
- let order_request_disabled = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- Some("bin-1"),
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(0),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(order_request_disabled.protocol_valid);
- assert!(order_request_disabled.marketplace_eligible);
- assert!(!order_request_disabled.checkout_enabled);
- assert!(order_request_disabled.primary_bin_verified);
- assert_eq!(
- order_request_disabled.reason_codes,
- vec!["listing_checkout_disabled", "listing_inventory_unavailable"]
- );
-
- let primary_bin_missing = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- None,
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(primary_bin_missing.protocol_valid);
- assert!(primary_bin_missing.marketplace_eligible);
- assert!(!primary_bin_missing.checkout_enabled);
- assert!(!primary_bin_missing.primary_bin_verified);
- assert_eq!(
- primary_bin_missing.reason_codes,
- vec!["listing_checkout_disabled", "listing_primary_bin_missing"]
- );
-
- let primary_bin_blank = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- Some(" "),
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert_eq!(
- primary_bin_blank.reason_codes,
- vec!["listing_checkout_disabled", "listing_primary_bin_missing"]
- );
-
- let primary_bin_invalid = MarketReadinessView::from_market_projection(
- Some(LISTING_ADDR),
- Some("missing-bin"),
- Some("bin-1"),
- Some("Eggs"),
- Some("eggs"),
- Some(1),
- 6.0,
- "USD",
- 1.0,
- );
- assert!(primary_bin_invalid.protocol_valid);
- assert!(primary_bin_invalid.marketplace_eligible);
- assert!(!primary_bin_invalid.checkout_enabled);
- assert!(!primary_bin_invalid.primary_bin_verified);
- assert_eq!(
- primary_bin_invalid.reason_codes,
- vec!["listing_checkout_disabled", "listing_primary_bin_invalid"]
- );
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobListView {
- pub state: String,
- pub source: String,
- pub rpc_url: String,
- pub count: usize,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub jobs: Vec<JobSummaryView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobGetView {
- pub state: String,
- pub source: String,
- pub rpc_url: String,
- pub lookup: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job: Option<JobDetailView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobWatchView {
- pub state: String,
- pub source: String,
- pub rpc_url: String,
- pub job_id: String,
- pub interval_ms: u64,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub frames: Vec<JobWatchFrameView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobSummaryView {
- pub id: String,
- pub command: String,
- pub state: String,
- pub terminal: bool,
- pub signer: String,
- pub requested_at_unix: u64,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub completed_at_unix: Option<u64>,
- pub recovered_after_restart: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobDetailView {
- pub id: String,
- pub command: String,
- pub state: String,
- pub terminal: bool,
- pub signer: String,
- pub requested_at_unix: u64,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub completed_at_unix: Option<u64>,
- pub recovered_after_restart: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
- pub delivery_policy: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub delivery_quorum: Option<usize>,
- pub relay_count: usize,
- pub acknowledged_relay_count: usize,
- pub required_acknowledged_relay_count: usize,
- pub attempt_count: usize,
- pub relay_outcome_summary: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub attempt_summaries: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct JobWatchFrameView {
- pub sequence: usize,
- pub observed_at_unix: u64,
- pub state: String,
- pub terminal: bool,
- pub signer: String,
- pub summary: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingNewView {
- pub state: String,
- pub source: String,
- pub file: String,
- pub listing_id: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_actor_source: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_d_tag: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub delivery_method: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingNewView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingValidateView {
- pub state: String,
- pub source: String,
- pub file: String,
- pub valid: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_actor_source: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_d_tag: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub issues: Vec<ListingValidationIssueView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingValidationIssueView {
- pub field: String,
- pub message: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub line: Option<usize>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingListView {
- pub state: String,
- pub source: String,
- pub count: usize,
- pub draft_dir: String,
- pub listings: Vec<ListingSummaryView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingListView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingAppRecordListView {
- pub state: String,
- pub source: String,
- pub count: usize,
- pub limit: u32,
- pub has_more: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub next_before_change_seq: Option<i64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub next_before_seq: Option<i64>,
- pub runtime_store_db: String,
- pub records: Vec<ListingAppRecordSummaryView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingAppRecordListView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingAppRecordSummaryView {
- pub record_id: String,
- pub seq: i64,
- pub change_seq: i64,
- pub superseded_count: usize,
- pub record_kind: String,
- pub status: String,
- pub source_runtime: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub owner_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub owner_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub title: Option<String>,
- pub exportable: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingAppRecordExportView {
- pub state: String,
- pub source: String,
- pub record_id: String,
- pub dry_run: bool,
- pub file: String,
- pub valid: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_actor_source: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_d_tag: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub issues: Vec<ListingValidationIssueView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingAppRecordExportView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "missing" => CommandDisposition::NotFound,
- "invalid" | "stale" | "unsupported" => CommandDisposition::ValidationFailed,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingSummaryView {
- pub id: String,
- pub state: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub title: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub category: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_actor_source: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_d_tag: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- pub updated_at_unix: u64,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub issues: Vec<ListingValidationIssueView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SellAddView {
- pub state: String,
- pub source: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub title: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub offer: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub price: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub stock: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_name: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub delivery_method: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SellAddView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SellShowView {
- pub state: String,
- pub source: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub title: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub category: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub offer: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub price: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub stock: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub delivery_method: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SellShowView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SellCheckView {
- pub state: String,
- pub source: String,
- pub file: String,
- pub valid: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_ref: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub issues: Vec<ListingValidationIssueView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SellCheckView {
- pub fn disposition(&self) -> CommandDisposition {
- CommandDisposition::Success
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SellMutationView {
- pub state: String,
- pub operation: String,
- pub source: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- pub listing_addr: String,
- #[serde(default)]
- pub dry_run: bool,
- #[serde(default)]
- pub deduplicated: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub transport_profile: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_status: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SellMutationView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SellDraftMutationView {
- pub state: String,
- pub operation: String,
- pub source: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- pub changed_label: String,
- pub changed_value: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SellDraftMutationView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingGetView {
- pub state: String,
- pub source: String,
- pub lookup: String,
- #[serde(flatten)]
- pub readiness: MarketReadinessView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub product_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub primary_bin_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub title: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub category: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub description: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub available: Option<FindQuantityView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub price: Option<FindPriceView>,
- pub provenance: FindResultProvenanceView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingGetView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingMutationView {
- pub state: String,
- pub operation: String,
- pub source: String,
- pub file: String,
- pub listing_id: String,
- pub listing_addr: String,
- pub seller_account_id: String,
- pub seller_pubkey: String,
- pub seller_actor_source: String,
- pub event_kind: u32,
- #[serde(default)]
- pub dry_run: bool,
- #[serde(default)]
- pub deduplicated: bool,
- #[serde(default)]
- pub target_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub attempted_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub accepted_transport_endpoints: Vec<String>,
- #[serde(default)]
- pub failed_transport_targets: Vec<TransportTargetFailureView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_status: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub idempotency_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub local_replica: Option<ListingMutationLocalReplicaView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job: Option<ListingMutationJobView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event: Option<ListingMutationEventView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingMutationView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingRebindView {
- pub state: String,
- pub source: String,
- pub file: String,
- pub listing_id: String,
- pub dry_run: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_seller_account_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_seller_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_seller_actor_source: Option<String>,
- pub to_seller_account_id: String,
- pub to_seller_pubkey: String,
- pub to_seller_actor_source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub seller_pubkey_changed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_listing_addr: Option<String>,
- pub to_listing_addr: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_addr_changed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub from_farm_d_tag: Option<String>,
- pub to_farm_d_tag: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub farm_d_tag_changed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl ListingRebindView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingMutationLocalReplicaView {
- pub state: String,
- pub store_state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ingest_outcome: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingMutationJobView {
- pub rpc_method: String,
- pub state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub idempotency_key: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub relay_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub acknowledged_relay_count: Option<usize>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ListingMutationEventView {
- pub kind: u32,
- pub author: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub created_at: Option<u64>,
- pub content: String,
- pub tags: Vec<Vec<String>>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signature: Option<String>,
- pub event_addr: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindResultView {
- pub id: String,
- pub product_key: String,
- #[serde(flatten)]
- pub readiness: MarketReadinessView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub listing_addr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub primary_bin_id: Option<String>,
- pub title: String,
- pub category: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub summary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub location_primary: Option<String>,
- pub available: FindQuantityView,
- pub price: FindPriceView,
- pub provenance: FindResultProvenanceView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub hyf: Option<FindResultHyfView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindResultHyfView {
- pub state: String,
- pub rewritten_query: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub query_terms: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindQuantityView {
- pub total_amount: f64,
- pub total_unit: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub label: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub available_amount: Option<i64>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindPriceView {
- pub amount: f64,
- pub currency: String,
- pub per_amount: f64,
- pub per_unit: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FindResultProvenanceView {
- pub origin: String,
- pub freshness: String,
- pub relay_count: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncFreshnessView {
- pub state: String,
- pub display: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub age_seconds: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_event_at: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub run: Option<SyncRunFreshnessView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncRunFreshnessView {
- pub scope: String,
- pub relay_set_fingerprint: String,
- pub relay_set_current: bool,
- pub last_state: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_attempted_at: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_successful_at: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_completed_at: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub stale_after_seconds: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub fetched_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ingested_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub skipped_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub unsupported_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub failed_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub failure_reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncQueueView {
- pub expected_count: usize,
- pub pending_count: usize,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub total_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub retryable_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub terminal_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub failed_terminal_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub deferred_until_implemented_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ready_signed_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub publishing_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_attempt_at_ms: Option<i64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_error: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalBackupView {
- pub state: String,
- pub source: String,
- pub backup_kind: String,
- pub canonical_store: String,
- pub destination: String,
- pub file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub event_store_file: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub outbox_file: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub manifest_file: Option<String>,
- pub size_bytes: u64,
- pub manifest: serde_json::Value,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl LocalBackupView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalRestoreView {
- pub state: String,
- pub source: String,
- pub restore_kind: String,
- pub canonical_store: String,
- pub backup_source: String,
- pub destination: String,
- pub event_store_file: String,
- pub outbox_file: String,
- pub manifest_file: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub destination_event_store_file: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub destination_outbox_file: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub restored_event_store_file: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub restored_outbox_file: Option<String>,
- pub manifest: serde_json::Value,
- pub verification: serde_json::Value,
- pub overwrite: bool,
- pub dry_run: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl LocalRestoreView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalExportView {
- pub state: String,
- pub source: String,
- pub format: String,
- pub file: String,
- pub records: usize,
- pub export_version: String,
- pub schema_hash: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl LocalExportView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportProfileView {
- pub state: String,
- pub source: String,
- pub profile_id: String,
- pub profile_kind: String,
- pub configured_state: String,
- pub profile_delivery_usable: bool,
- pub message: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub nostr_relays: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reticulum_behavior: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reticulum_scope: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reticulum_agent_endpoint: Option<String>,
- pub transport_statuses: Vec<TransportRuntimeStatusView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportStatusInspectView {
- pub state: String,
- pub source: String,
- pub active_profile: TransportProfileSummaryView,
- pub transports: Vec<TransportRuntimeStatusView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportProfileSummaryView {
- pub profile_id: String,
- pub profile_kind: String,
- pub configured_state: String,
- pub profile_delivery_usable: bool,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportRuntimeStatusView {
- pub transport: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub profile_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub endpoint_uri: Option<String>,
- pub configured: bool,
- pub implementation: String,
- pub maturity: String,
- pub availability: String,
- pub usable_for_delivery: bool,
- pub capabilities: TransportOperationCapabilitiesView,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportOperationCapabilitiesView {
- pub deliver: bool,
- pub fetch: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportDeliveryInspectView {
- pub state: String,
- pub source: String,
- pub transport_profile: String,
- pub total_count: i64,
- pub pending_count: i64,
- pub retryable_count: i64,
- pub terminal_count: i64,
- pub deferred_until_implemented_count: i64,
- pub ready_signed_count: i64,
- pub publishing_count: i64,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_attempt_at_ms: Option<i64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub last_error: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportDeliveryRetryView {
- pub state: String,
- pub source: String,
- pub attempted_events: usize,
- pub published_events: usize,
- pub retryable_events: usize,
- pub terminal_events: usize,
- pub target_count: usize,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MeshScopeView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub implementation: String,
- pub message: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MeshStatusView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub transport: String,
- pub configured: bool,
- pub implementation: String,
- pub usable_for_delivery: bool,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MeshPolicyCheckView {
- pub state: String,
- pub source: String,
- pub scope: String,
- pub policy: String,
- pub transport: String,
- pub usable_for_delivery: bool,
- pub decision: String,
- pub deny_reason: String,
- pub privacy_class: String,
- pub payload_bytes: u64,
- pub frame_bytes: u64,
- pub max_payload_bytes: u64,
- pub max_frame_bytes: u64,
- pub compression: String,
- pub custom_scopes_enabled: bool,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RpcStatusView {
- pub state: String,
- pub source: String,
- pub url: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub auth_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub default_signer_mode: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub supported_signer_modes: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub daemon_execution_enabled: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub daemon_execution_ready: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub relay_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub available_nip46_signer_sessions: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub job_status_retention: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub retained_jobs: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub accepted_jobs: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub published_jobs: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub failed_jobs: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub recovered_failed_jobs: Option<usize>,
- pub session_surface_enabled: bool,
- pub methods_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RpcSessionsView {
- pub state: String,
- pub source: String,
- pub url: String,
- pub count: usize,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub sessions: Vec<RpcSessionView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct RpcSessionView {
- pub session_id: String,
- pub role: String,
- pub client_pubkey: String,
- pub signer_pubkey: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub user_pubkey: Option<String>,
- pub relay_count: usize,
- pub permissions_count: usize,
- pub auth_required: bool,
- pub authorized: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expires_in_secs: Option<u64>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncStatusView {
- pub state: String,
- pub source: String,
- pub local_root: String,
- pub replica_store: String,
- pub configured_transport_target_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub configured_transport_targets: Vec<SyncTransportTargetView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub transport_statuses: Vec<SyncTransportStatusInspectView>,
- pub publish_policy: String,
- pub freshness: SyncFreshnessView,
- pub queue: SyncQueueView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SyncStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncActionView {
- pub direction: String,
- pub state: String,
- pub source: String,
- pub local_root: String,
- pub replica_store: String,
- pub configured_transport_target_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub configured_transport_targets: Vec<SyncTransportTargetView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub transport_statuses: Vec<SyncTransportStatusInspectView>,
- pub publish_policy: String,
- pub freshness: SyncFreshnessView,
- pub queue: SyncQueueView,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub target_transport_endpoints: Vec<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub attempted_transport_endpoints: Vec<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub accepted_transport_endpoints: Vec<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub failed_transport_targets: Vec<TransportTargetFailureView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub fetched_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ingested_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub publishable_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub published_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub skipped_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub unsupported_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub failed_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub publish_plan: Option<SyncPublishPlanView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason_code: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncTransportTargetView {
- pub transport_kind: String,
- pub endpoint_uri: String,
- pub endpoint_fingerprint: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_scope: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_label: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncTransportStatusInspectView {
- pub transport: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub profile_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub endpoint_uri: Option<String>,
- pub configured: bool,
- pub implementation: String,
- pub maturity: String,
- pub availability: String,
- pub usable_for_delivery: bool,
- pub capabilities: TransportOperationCapabilitiesView,
- pub message: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct TransportTargetFailureView {
- pub transport_kind: String,
- pub endpoint_uri: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_scope: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_label: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub transport_outcome_kind: Option<String>,
- pub reason: String,
-}
-
-impl SyncActionView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "partial" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncPublishPlanView {
- pub selected_author: String,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub event_kinds: Vec<SyncPublishPlanKindView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub authors: Vec<SyncPublishPlanAuthorView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncPublishPlanKindView {
- pub kind: u32,
- pub pending_count: usize,
- pub publishable_count: usize,
- pub skipped_count: usize,
- pub unsupported_count: usize,
- pub failed_count: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncPublishPlanAuthorView {
- pub author: String,
- pub eligibility: String,
- pub pending_count: usize,
- pub publishable_count: usize,
- pub skipped_count: usize,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncWatchView {
- pub state: String,
- pub source: String,
- pub interval_ms: u64,
- pub frames: Vec<SyncWatchFrameView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub actions: Vec<String>,
-}
-
-impl SyncWatchView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SyncWatchFrameView {
- pub sequence: usize,
- pub observed_at: u64,
- pub state: String,
- pub configured_transport_target_count: usize,
- pub freshness: SyncFreshnessView,
- pub queue: SyncQueueView,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SignerStatusView {
- pub mode: String,
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_account_id: Option<String>,
- pub account_resolution: AccountResolutionView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- pub binding: SignerBindingStatusView,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub write_kinds: Vec<SignerWriteKindReadinessView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub local: Option<LocalSignerStatusView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub myc: Option<MycStatusView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SignerWriteKindReadinessView {
- pub command: String,
- pub event_kind: u32,
- pub permission: String,
- pub ready: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SignerSessionActionView {
- pub action: String,
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub session_id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub mode: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub remote_signer_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub client_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub user_pubkey: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub relays: Vec<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub permissions: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub auth_required: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub authorized: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub auth_url: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expires_in_secs: Option<u64>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub pubkey: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub replayed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub required: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub closed: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
-}
-
-impl SignerStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "degraded" => CommandDisposition::ExternalUnavailable,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- "error" => CommandDisposition::InternalError,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct LocalSignerStatusView {
- pub account_id: String,
- pub public_identity: IdentityPublicView,
- pub availability: String,
- pub secret_backed: bool,
- pub backend: String,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct SignerBindingStatusView {
- pub capability_id: String,
- pub provider_runtime_id: String,
- pub binding_model: String,
- pub state: String,
- pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target_kind: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub target: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub managed_account_ref: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signer_session_ref: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub resolved_session_ref: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub matched_session_count: Option<usize>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MycStatusView {
- pub executable: String,
- pub state: String,
- pub source: String,
- pub service_status: Option<String>,
- pub ready: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub reason: Option<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub reasons: Vec<String>,
- pub remote_session_count: usize,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub local_signer: Option<LocalSignerStatusView>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub remote_sessions: Vec<MycRemoteSessionView>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub custody: Option<MycCustodyView>,
-}
-
-impl MycStatusView {
- pub fn disposition(&self) -> CommandDisposition {
- match self.state.as_str() {
- "unconfigured" => CommandDisposition::Unconfigured,
- "degraded" => CommandDisposition::ExternalUnavailable,
- "unavailable" => CommandDisposition::ExternalUnavailable,
- _ => CommandDisposition::Success,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MycRemoteSessionView {
- pub connection_id: String,
- pub signer_identity: IdentityPublicView,
- pub user_identity: IdentityPublicView,
- pub relay_count: usize,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub permissions: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MycCustodyView {
- pub signer: MycCustodyIdentityView,
- pub user: MycCustodyIdentityView,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub discovery_app: Option<MycCustodyIdentityView>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct MycCustodyIdentityView {
- pub resolved: bool,
- pub selected_account_id: Option<String>,
- pub selected_account_state: Option<String>,
- pub identity_id: Option<String>,
- pub public_key_hex: Option<String>,
- pub error: Option<String>,
-}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -1,144 +1,43 @@
use assert_cmd::Command;
-use radroots_runtime_contract_v1::RUNTIME_OPERATION_DESCRIPTORS_V1;
use serde_json::Value;
-const UUID_V7: &str = "01890f78-9abc-7def-8abc-123456789abc";
-
fn radroots() -> Command {
Command::cargo_bin("radroots").expect("radroots binary")
}
-fn json_output(args: &[&str]) -> Value {
- let output = radroots()
- .args(args)
- .assert()
- .success()
- .get_output()
- .clone();
- serde_json::from_slice(&output.stdout).expect("json stdout")
-}
-
-fn json_failure(args: &[&str]) -> Value {
- let output = radroots()
- .args(args)
- .assert()
- .failure()
- .get_output()
- .clone();
- serde_json::from_slice(&output.stdout).expect("json failure stdout")
-}
-
-#[test]
-fn generated_v1_read_operation_runs_from_binary() {
- let value = json_output(&["--format", "json", "profile", "inspect"]);
-
- assert_eq!(value["operation_id"], "profile.inspect");
- assert_eq!(value["kind"], "profile.inspect");
- assert_eq!(value["status"], "ok");
-}
-
-#[test]
-fn binary_rejects_retired_command_surfaces() {
- for args in [
- &["workspace", "get"][..],
- &["config", "get"],
- &["mesh", "status"],
- &["trade", "submit"],
- &["listing", "archive"],
- &["validation", "receipt", "list"],
- ] {
- radroots().args(args).assert().failure();
- }
-}
-
#[test]
-fn reads_reject_idempotency_keys() {
- let value = json_failure(&[
- "--format",
- "json",
- "--idempotency-key",
- UUID_V7,
- "profile",
- "inspect",
- ]);
-
- assert_eq!(value["operation_id"], "profile.inspect");
- assert_eq!(value["status"], "error");
- assert_eq!(value["reason_code"], "invalid_input");
- assert!(
- value["errors"][0]["message"]
- .as_str()
- .expect("error message")
- .contains("forbids idempotency")
- );
+fn help_exposes_the_resource_command_tree() {
+ let output = radroots().arg("--help").output().expect("help output");
+ assert!(output.status.success());
+ let stdout = String::from_utf8(output.stdout).expect("UTF-8 help");
+ assert!(stdout.contains("health"));
+ assert!(stdout.contains("profile"));
}
#[test]
-fn mutations_require_uuid_v7_idempotency_keys() {
- let missing = json_failure(&["--format", "json", "--yes", "profile", "reset"]);
- assert_eq!(missing["operation_id"], "profile.reset");
- assert!(
- missing["errors"][0]["message"]
- .as_str()
- .expect("missing idempotency message")
- .contains("requires UUIDv7 idempotency")
- );
-
- let invalid = json_failure(&[
- "--format",
- "json",
- "--yes",
- "--idempotency-key",
- "profile-reset",
- "profile",
- "reset",
- ]);
- assert_eq!(invalid["operation_id"], "profile.reset");
- assert!(
- invalid["errors"][0]["message"]
- .as_str()
- .expect("invalid idempotency message")
- .contains("invalid UUIDv7 idempotency")
- );
-}
-
-#[test]
-fn approval_required_operations_accept_explicit_yes_confirmation() {
- let value = json_output(&[
- "--format",
- "json",
- "--dry-run",
- "--yes",
- "--idempotency-key",
- UUID_V7,
- "profile",
- "reset",
- ]);
-
- assert_eq!(value["operation_id"], "profile.reset");
+fn health_uses_the_registry_backed_facade() {
+ let output = radroots()
+ .args(["health", "inspect", "--format", "json"])
+ .output()
+ .expect("health output");
+ assert!(output.status.success());
+ let value: Value = serde_json::from_slice(&output.stdout).expect("JSON envelope");
+ assert_eq!(value["schema"], "radroots.cli.output.v1");
+ assert_eq!(value["operation_id"], "health.inspect");
assert_eq!(value["status"], "ok");
- assert_eq!(value["dry_run"], true);
- assert_eq!(value["idempotency_key"], UUID_V7);
+ assert_eq!(value["data"]["ready"], true);
+ assert_eq!(value["data"]["profile"]["artifact_graph"], "registry");
}
#[test]
-fn binary_v1_catalog_has_no_retired_operation_ids() {
- let operation_ids = RUNTIME_OPERATION_DESCRIPTORS_V1
- .iter()
- .map(|descriptor| descriptor.operation_id.as_str())
- .collect::<Vec<_>>();
-
- for retired in [
- vec!["workspace", "get"],
- vec!["config", "get"],
- vec!["mesh", "status"],
- vec!["trade", "submit"],
- vec!["listing", "archive"],
- vec!["validation", "receipt", "list"],
- ]
- .into_iter()
- .map(|parts| parts.join("."))
- {
- assert!(!operation_ids.contains(&retired.as_str()), "{retired}");
- }
+fn retired_runtime_operations_fail_closed() {
+ let output = radroots()
+ .args(["account", "list", "--format", "json"])
+ .output()
+ .expect("account output");
+ assert_eq!(output.status.code(), Some(2));
+ let value: Value = serde_json::from_slice(&output.stdout).expect("JSON envelope");
+ assert_eq!(value["operation_id"], "account.list");
+ assert_eq!(value["status"], "error");
+ assert_eq!(value["error"]["code"], "unsupported_operation");
}