commit 448a68aa98ce009a6eb69221a6f471f379769b8e
parent 7e2fb7b739211dd5a653863aa44fb5d67bc002f3
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 21:17:12 +0000
app: establish bootstrap runtime
- add a thin binary entrypoint that delegates startup through the library surface
- introduce typed config, logging, and error modules for repo-root service bootstrap
- add runtime path preparation and startup snapshot wiring for signer-owned state
- validate with cargo metadata --format-version 1 --no-deps, cargo check, cargo test, and cargo fmt --check
Diffstat:
| M | Cargo.lock | | | 667 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | Cargo.toml | | | 10 | ++++++++++ |
| A | src/app/mod.rs | | | 54 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/app/runtime.rs | | | 128 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/config.rs | | | 194 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/error.rs | | | 35 | +++++++++++++++++++++++++++++++++++ |
| A | src/lib.rs | | | 18 | ++++++++++++++++++ |
| A | src/logging.rs | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
| M | src/main.rs | | | 7 | ++++++- |
9 files changed, 1150 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,5 +3,672 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.183"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
name = "myc"
version = "0.1.0"
+dependencies = [
+ "serde",
+ "tempfile",
+ "thiserror",
+ "toml",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[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",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[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.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
@@ -12,3 +12,13 @@ resolver = "2"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+thiserror = "2.0"
+toml = "0.8"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
+
+[dev-dependencies]
+tempfile = "3.17"
diff --git a/src/app/mod.rs b/src/app/mod.rs
@@ -0,0 +1,54 @@
+pub mod runtime;
+
+use crate::config::MycConfig;
+use crate::error::MycError;
+
+pub use runtime::{MycRuntime, MycRuntimePaths, MycStartupSnapshot};
+
+#[derive(Debug, Clone)]
+pub struct MycApp {
+ runtime: MycRuntime,
+}
+
+impl MycApp {
+ pub fn bootstrap(config: MycConfig) -> Result<Self, MycError> {
+ Ok(Self {
+ runtime: MycRuntime::bootstrap(config)?,
+ })
+ }
+
+ pub fn runtime(&self) -> &MycRuntime {
+ &self.runtime
+ }
+
+ pub fn snapshot(&self) -> MycStartupSnapshot {
+ self.runtime.snapshot()
+ }
+
+ pub fn run(self) -> Result<(), MycError> {
+ self.runtime.run()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use crate::config::MycConfig;
+
+ use super::MycApp;
+
+ #[test]
+ fn app_bootstrap_preserves_runtime_snapshot() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let mut config = MycConfig::default();
+ config.paths.state_dir = PathBuf::from(temp.path()).join("state");
+
+ let app = MycApp::bootstrap(config).expect("bootstrap");
+ let snapshot = app.snapshot();
+
+ assert!(snapshot.state_dir.ends_with("state"));
+ assert!(snapshot.audit_dir.ends_with("audit"));
+ assert!(snapshot.signer_state_path.ends_with("signer-state.json"));
+ }
+}
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
@@ -0,0 +1,128 @@
+use std::fs;
+use std::path::PathBuf;
+
+use crate::config::MycConfig;
+use crate::error::MycError;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MycRuntimePaths {
+ pub state_dir: PathBuf,
+ pub audit_dir: PathBuf,
+ pub signer_state_path: PathBuf,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MycStartupSnapshot {
+ pub instance_name: String,
+ pub log_filter: String,
+ pub state_dir: PathBuf,
+ pub audit_dir: PathBuf,
+ pub signer_state_path: PathBuf,
+}
+
+#[derive(Debug, Clone)]
+pub struct MycRuntime {
+ config: MycConfig,
+ paths: MycRuntimePaths,
+}
+
+impl MycRuntime {
+ pub fn bootstrap(config: MycConfig) -> Result<Self, MycError> {
+ config.validate()?;
+
+ let runtime = Self {
+ paths: MycRuntimePaths::from_config(&config),
+ config,
+ };
+ runtime.prepare_filesystem()?;
+ Ok(runtime)
+ }
+
+ pub fn paths(&self) -> &MycRuntimePaths {
+ &self.paths
+ }
+
+ pub fn config(&self) -> &MycConfig {
+ &self.config
+ }
+
+ pub fn snapshot(&self) -> MycStartupSnapshot {
+ MycStartupSnapshot {
+ instance_name: self.config.service.instance_name.clone(),
+ log_filter: self.config.logging.filter.clone(),
+ state_dir: self.paths.state_dir.clone(),
+ audit_dir: self.paths.audit_dir.clone(),
+ signer_state_path: self.paths.signer_state_path.clone(),
+ }
+ }
+
+ pub fn run(self) -> Result<(), MycError> {
+ let snapshot = self.snapshot();
+ tracing::info!(
+ instance_name = %snapshot.instance_name,
+ state_dir = %snapshot.state_dir.display(),
+ audit_dir = %snapshot.audit_dir.display(),
+ signer_state_path = %snapshot.signer_state_path.display(),
+ "myc runtime bootstrapped"
+ );
+ Ok(())
+ }
+
+ fn prepare_filesystem(&self) -> Result<(), MycError> {
+ fs::create_dir_all(&self.paths.state_dir).map_err(|source| MycError::CreateDir {
+ path: self.paths.state_dir.clone(),
+ source,
+ })?;
+ fs::create_dir_all(&self.paths.audit_dir).map_err(|source| MycError::CreateDir {
+ path: self.paths.audit_dir.clone(),
+ source,
+ })?;
+ Ok(())
+ }
+}
+
+impl MycRuntimePaths {
+ fn from_config(config: &MycConfig) -> Self {
+ let state_dir = config.paths.state_dir.clone();
+ Self {
+ signer_state_path: state_dir.join("signer-state.json"),
+ audit_dir: state_dir.join("audit"),
+ state_dir,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use crate::config::MycConfig;
+
+ use super::MycRuntime;
+
+ #[test]
+ fn bootstrap_creates_runtime_directories() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let mut config = MycConfig::default();
+ config.paths.state_dir = PathBuf::from(temp.path()).join("state");
+
+ let runtime = MycRuntime::bootstrap(config).expect("runtime");
+ assert!(runtime.paths().state_dir.is_dir());
+ assert!(runtime.paths().audit_dir.is_dir());
+ assert!(
+ runtime
+ .paths()
+ .signer_state_path
+ .ends_with("signer-state.json")
+ );
+ }
+
+ #[test]
+ fn bootstrap_rejects_invalid_config() {
+ let mut config = MycConfig::default();
+ config.service.instance_name.clear();
+
+ let err = MycRuntime::bootstrap(config).expect_err("invalid config");
+ assert!(err.to_string().contains("service.instance_name"));
+ }
+}
diff --git a/src/config.rs b/src/config.rs
@@ -0,0 +1,194 @@
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use serde::{Deserialize, Serialize};
+use tracing_subscriber::EnvFilter;
+
+use crate::error::MycError;
+
+pub const DEFAULT_CONFIG_PATH: &str = "config.toml";
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct MycConfig {
+ pub service: MycServiceConfig,
+ pub logging: MycLoggingConfig,
+ pub paths: MycPathsConfig,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct MycServiceConfig {
+ pub instance_name: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct MycLoggingConfig {
+ pub filter: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct MycPathsConfig {
+ pub state_dir: PathBuf,
+}
+
+impl Default for MycConfig {
+ fn default() -> Self {
+ Self {
+ service: MycServiceConfig::default(),
+ logging: MycLoggingConfig::default(),
+ paths: MycPathsConfig::default(),
+ }
+ }
+}
+
+impl Default for MycServiceConfig {
+ fn default() -> Self {
+ Self {
+ instance_name: "myc".to_owned(),
+ }
+ }
+}
+
+impl Default for MycLoggingConfig {
+ fn default() -> Self {
+ Self {
+ filter: "info,myc=info".to_owned(),
+ }
+ }
+}
+
+impl Default for MycPathsConfig {
+ fn default() -> Self {
+ Self {
+ state_dir: PathBuf::from("var"),
+ }
+ }
+}
+
+impl MycConfig {
+ pub fn load_from_default_path_if_exists() -> Result<Self, MycError> {
+ Self::load_from_path_if_exists(DEFAULT_CONFIG_PATH)
+ }
+
+ pub fn load_from_path_if_exists(path: impl AsRef<Path>) -> Result<Self, MycError> {
+ let path = path.as_ref();
+ if !path.exists() {
+ let config = Self::default();
+ config.validate()?;
+ return Ok(config);
+ }
+
+ Self::load_from_path(path)
+ }
+
+ pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, MycError> {
+ let path = path.as_ref();
+ let value = fs::read_to_string(path).map_err(|source| MycError::ConfigIo {
+ path: path.to_path_buf(),
+ source,
+ })?;
+ Self::from_toml_str_with_source(&value, path)
+ }
+
+ pub fn from_toml_str(value: &str) -> Result<Self, MycError> {
+ Self::from_toml_str_with_source(value, Path::new("<inline>"))
+ }
+
+ pub fn validate(&self) -> Result<(), MycError> {
+ if self.service.instance_name.trim().is_empty() {
+ return Err(MycError::InvalidConfig(
+ "service.instance_name must not be empty".to_owned(),
+ ));
+ }
+
+ if self.logging.filter.trim().is_empty() {
+ return Err(MycError::InvalidConfig(
+ "logging.filter must not be empty".to_owned(),
+ ));
+ }
+
+ EnvFilter::try_new(self.logging.filter.clone()).map_err(|source| {
+ MycError::InvalidLogFilter {
+ filter: self.logging.filter.clone(),
+ source,
+ }
+ })?;
+
+ if self.paths.state_dir.as_os_str().is_empty() {
+ return Err(MycError::InvalidConfig(
+ "paths.state_dir must not be empty".to_owned(),
+ ));
+ }
+
+ Ok(())
+ }
+
+ fn from_toml_str_with_source(value: &str, path: &Path) -> Result<Self, MycError> {
+ let config: Self = toml::from_str(value).map_err(|source| MycError::ConfigParse {
+ path: path.to_path_buf(),
+ source,
+ })?;
+ config.validate()?;
+ Ok(config)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn default_config_is_stable() {
+ let config = MycConfig::default();
+ assert_eq!(config.service.instance_name, "myc");
+ assert_eq!(config.logging.filter, "info,myc=info");
+ assert_eq!(config.paths.state_dir, PathBuf::from("var"));
+ }
+
+ #[test]
+ fn parse_config_from_toml_overrides_defaults() {
+ let config = MycConfig::from_toml_str(
+ r#"
+ [service]
+ instance_name = "myc-dev"
+
+ [logging]
+ filter = "debug,myc=trace"
+
+ [paths]
+ state_dir = "/tmp/myc"
+ "#,
+ )
+ .expect("config");
+
+ assert_eq!(config.service.instance_name, "myc-dev");
+ assert_eq!(config.logging.filter, "debug,myc=trace");
+ assert_eq!(config.paths.state_dir, PathBuf::from("/tmp/myc"));
+ }
+
+ #[test]
+ fn load_from_missing_path_returns_default_config() {
+ let temp = tempfile::tempdir().expect("tempdir");
+ let config = MycConfig::load_from_path_if_exists(temp.path().join("missing.toml"))
+ .expect("missing path fallback");
+
+ assert_eq!(config, MycConfig::default());
+ }
+
+ #[test]
+ fn parse_rejects_unknown_fields() {
+ let err = MycConfig::from_toml_str(
+ r#"
+ [service]
+ instance_name = "myc-dev"
+ extra = "nope"
+ "#,
+ )
+ .expect_err("unknown field");
+
+ assert!(err.to_string().contains("config parse error"));
+ }
+}
diff --git a/src/error.rs b/src/error.rs
@@ -0,0 +1,35 @@
+use std::path::PathBuf;
+
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum MycError {
+ #[error("config io error at {path}: {source}")]
+ ConfigIo {
+ path: PathBuf,
+ #[source]
+ source: std::io::Error,
+ },
+ #[error("config parse error at {path}: {source}")]
+ ConfigParse {
+ path: PathBuf,
+ #[source]
+ source: toml::de::Error,
+ },
+ #[error("invalid config: {0}")]
+ InvalidConfig(String),
+ #[error("invalid log filter `{filter}`: {source}")]
+ InvalidLogFilter {
+ filter: String,
+ #[source]
+ source: tracing_subscriber::filter::ParseError,
+ },
+ #[error("logging already initialized")]
+ LoggingAlreadyInitialized,
+ #[error("failed to create directory {path}: {source}")]
+ CreateDir {
+ path: PathBuf,
+ #[source]
+ source: std::io::Error,
+ },
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,18 @@
+#![forbid(unsafe_code)]
+
+pub mod app;
+pub mod config;
+pub mod error;
+pub mod logging;
+
+pub use app::{MycApp, MycRuntime, MycRuntimePaths, MycStartupSnapshot};
+pub use config::{
+ DEFAULT_CONFIG_PATH, MycConfig, MycLoggingConfig, MycPathsConfig, MycServiceConfig,
+};
+pub use error::MycError;
+
+pub fn run() -> Result<(), MycError> {
+ let config = MycConfig::load_from_default_path_if_exists()?;
+ logging::init_logging(&config.logging)?;
+ MycApp::bootstrap(config)?.run()
+}
diff --git a/src/logging.rs b/src/logging.rs
@@ -0,0 +1,38 @@
+use tracing_subscriber::EnvFilter;
+
+use crate::config::MycLoggingConfig;
+use crate::error::MycError;
+
+pub fn build_env_filter(filter: &str) -> Result<EnvFilter, MycError> {
+ EnvFilter::try_new(filter).map_err(|source| MycError::InvalidLogFilter {
+ filter: filter.to_owned(),
+ source,
+ })
+}
+
+pub fn init_logging(config: &MycLoggingConfig) -> Result<(), MycError> {
+ let filter = build_env_filter(&config.filter)?;
+ let subscriber = tracing_subscriber::fmt()
+ .with_env_filter(filter)
+ .with_target(true)
+ .finish();
+
+ tracing::subscriber::set_global_default(subscriber)
+ .map_err(|_| MycError::LoggingAlreadyInitialized)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn build_env_filter_accepts_valid_filter() {
+ assert!(build_env_filter("info,myc=debug").is_ok());
+ }
+
+ #[test]
+ fn build_env_filter_rejects_invalid_filter() {
+ let err = build_env_filter("info,myc=[").expect_err("invalid filter");
+ assert!(err.to_string().contains("invalid log filter"));
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -1,3 +1,8 @@
#![forbid(unsafe_code)]
-fn main() {}
+fn main() {
+ if let Err(err) = myc::run() {
+ eprintln!("myc: {err}");
+ std::process::exit(1);
+ }
+}