From ed0594e73e10bef797945d1a91d2ac83352fc80a Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:19:23 +0000 Subject: [PATCH 1/7] build: add sentry 0.48 (rustls, error-tracking only) - default-features=false drops native-tls + debug-images (both default) - features: reqwest + rustls transport, backtrace/contexts/panic - verified openssl-free: cargo tree -i openssl-sys / native-tls empty Co-Authored-By: Claude --- .gitignore | 4 + Cargo.lock | 1591 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 11 + 3 files changed, 1566 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 4f7e230..d20ceb7 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ config.toml # Personal, untracked prompt/skill copies (generic versions live in docs/prompts/) /tmp/ + +.claude-task-master/ +claudetm.log +goal.txt diff --git a/Cargo.lock b/Cargo.lock index e192594..98d58d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -111,6 +126,29 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "axum" version = "0.8.9" @@ -163,6 +201,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" @@ -184,6 +237,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -203,6 +265,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -212,6 +276,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.1" @@ -275,12 +345,41 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -349,6 +448,22 @@ dependencies = [ "syn", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -359,6 +474,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -405,6 +547,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -414,6 +562,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -519,8 +673,24 @@ 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", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -531,10 +701,35 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -565,6 +760,23 @@ 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 = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "1.4.2" @@ -620,6 +832,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -628,6 +841,22 @@ dependencies = [ "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", ] [[package]] @@ -636,13 +865,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -669,12 +906,114 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -685,6 +1024,12 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -697,6 +1042,65 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.103" @@ -737,6 +1141,12 @@ 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" @@ -752,6 +1162,12 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[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" @@ -781,6 +1197,7 @@ dependencies = [ "rpassword", "rusqlite", "schemars", + "sentry", "serde", "serde_json", "sha2", @@ -806,6 +1223,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.2.1" @@ -818,14 +1244,32 @@ dependencies = [ ] [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "nix" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "windows-sys 0.61.2", + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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" @@ -835,6 +1279,174 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -847,6 +1459,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -894,6 +1528,21 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[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" @@ -912,6 +1561,62 @@ 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", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.46" @@ -921,6 +1626,12 @@ 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" @@ -934,10 +1645,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.1" @@ -959,6 +1680,16 @@ dependencies = [ "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" @@ -968,6 +1699,15 @@ 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" @@ -1003,6 +1743,18 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1020,6 +1772,60 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rmcp" version = "1.8.0" @@ -1099,6 +1905,27 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1112,6 +1939,83 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1124,6 +2028,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "1.2.1" @@ -1156,6 +2078,132 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "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 = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "sentry" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1975064d9f3d9d87a7c8f0371f7fac10d876906ca3e39faad72e61c263ff9b87" +dependencies = [ + "cfg_aliases", + "httpdate", + "reqwest", + "rustls", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq", +] + +[[package]] +name = "sentry-backtrace" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134f552b6b147f77aeee46ece875d67a8bfc1d56fe3860d78446ed4c25bb5f9f" +dependencies = [ + "backtrace", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98482b938fb1f31ecd23506fb7f08b2ba20d60b9cc72581b1205a9acd31d32fa" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cff96247f4dc36867511d108709e703eb354143e7893319d763d2dd1edb4f48" +dependencies = [ + "rand 0.9.4", + "sentry-types", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "sentry-panic" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de3f4a1fc77d4c57e8bacd8ef064c26aaa88ea5b2541a761d37c7c3703b9a9" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc59b27dae3bb495e37e6d62d252214840a2e7e1875531ee9fa2953df8985537" +dependencies = [ + "bitflags", + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.48.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7e78132ccfb4a1c777b959fb2944d1f6d5145bffe6be2a98ee6b25d244f72c" +dependencies = [ + "debugid", + "hex", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror", + "time", + "url", + "uuid", +] + [[package]] name = "serde" version = "1.0.228" @@ -1278,6 +2326,22 @@ dependencies = [ "libc", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1313,12 +2377,24 @@ dependencies = [ "pin-project-lite", ] +[[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 = "syn" version = "2.0.118" @@ -1334,49 +2410,118 @@ dependencies = [ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +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", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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 = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] -name = "tempfile" -version = "3.27.0" +name = "time-macros" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "num-conv", + "time-core", ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "thiserror-impl", + "displaydoc", + "zerovec", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "proc-macro2", - "quote", - "syn", + "tinyvec_macros", ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" @@ -1406,6 +2551,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1487,6 +2642,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.11" +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", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1561,24 +2734,98 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1593,6 +2840,7 @@ checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -1614,12 +2862,40 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -1633,6 +2909,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[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" @@ -1665,6 +2951,53 @@ dependencies = [ "unicode-ident", ] +[[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-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1724,13 +3057,31 @@ 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.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "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]] @@ -1748,14 +3099,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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]] @@ -1764,48 +3132,96 @@ 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" @@ -1815,6 +3231,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.52" @@ -1835,6 +3286,66 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 407d5f1..d83df65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,17 @@ anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# error tracking → GlitchTip (Sentry-protocol). rustls transport, never +# native-tls: `default-features = false` drops `native-tls` + `debug-images` +# (both default). Re-enable only error-tracking integrations + reqwest/rustls. +sentry = { version = "0.48", default-features = false, features = [ + "reqwest", + "rustls", + "backtrace", + "contexts", + "panic", +] } + [dev-dependencies] tempfile = "3" tower = { version = "0.5", features = ["util"] } From f0b251961dafab79b32644829539fdd208a35fc7 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:39:31 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat(sentry):=20add=20src/sentry.rs=20?= =?UTF-8?q?=E2=80=94=20init=20+=20fail-closed=20before=5Fsend=20scrubber?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GlitchTip error tracking: ClientOptions (dsn from SENTRY_DSN env, env from SENTRY_ENVIRONMENT, release=CARGO_PKG_VERSION, send_default_pii=false, traces_sample_rate=0.0) with a before_send that scrubs every event. - Fail-closed scrubber: URL user:pass@host creds, secret key=value/key: value (password|passwd|pwd|secret|token|dsn|pass), and PEM private-key blocks → ***REDACTED***. If any regex fails to compile, paranoid mode redacts whole strings. No unwrap/expect outside tests; init never panics (DSN parsed first, malformed → disabled no-op client, app still runs). - scrub_str (pure, testable), scrub_value (recursive JSON), scrub_event (message/exception/breadcrumb/extra). 10 unit tests cover all three patterns, paranoid mode, DSN parse, JSON recursion, and the no-op path. - Module wired via `mod sentry;`; init() call lands with the main.rs init-first refactor (next task) — guarded with #![allow(dead_code)] until then. Co-Authored-By: Claude --- Cargo.lock | 1 + Cargo.toml | 4 + src/main.rs | 1 + src/sentry.rs | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 src/sentry.rs diff --git a/Cargo.lock b/Cargo.lock index 98d58d9..124775c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,6 +1193,7 @@ dependencies = [ "chrono", "clap", "rand 0.8.6", + "regex", "rmcp", "rpassword", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index d83df65..0b73acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# error-tracking event scrubbing (credential redaction before send). Already a +# transitive dep of `sentry`; declared so `src/sentry.rs` can name it directly. +regex = "1" + # error tracking → GlitchTip (Sentry-protocol). rustls transport, never # native-tls: `default-features = false` drops `native-tls` + `debug-images` # (both default). Re-enable only error-tracking integrations + reqwest/rustls. diff --git a/src/main.rs b/src/main.rs index 3ec672e..150352d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod config; mod db; mod jobs; mod oauth; +mod sentry; mod tools; use std::sync::Arc; diff --git a/src/sentry.rs b/src/sentry.rs new file mode 100644 index 0000000..481f69c --- /dev/null +++ b/src/sentry.rs @@ -0,0 +1,390 @@ +//! GlitchTip (Sentry-protocol) error tracking: init + a fail-closed `before_send` +//! scrubber. Error tracking ONLY — no traces, no profiling, no PII. +//! +//! This server handles SSH credentials (`MCP_SSH_PASS`, keys, host/user info), so +//! every outgoing event is redacted before it leaves the process. Scrubbing is +//! paranoid by construction: if a redaction regex ever fails to compile, the whole +//! string is replaced rather than risk a leak. +//! +//! Init is owned by `main` (it binds the guard for the program lifetime); this +//! module never logs or returns secrets. + +// Nothing here is referenced until `main` binds the guard (next change: init must +// run before the tokio runtime starts, so the wiring lands with that refactor). +// Drop this allow once `init()` is called from `main`. +#![allow(dead_code)] + +use std::sync::Arc; + +use regex::{Captures, Regex}; +use sentry::protocol::Event; +use sentry::types::Dsn; +use sentry::{BeforeCallback, ClientOptions}; +use serde_json::Value; +use tracing::warn; + +/// Literal every redaction replaces a secret with. +const REDACTED: &str = "***REDACTED***"; + +/// Key names whose value is always secret (case-insensitive). Mirrors the free-text +/// regex so structured JSON keys and ad-hoc `key=value` text agree on what's secret. +const SECRET_KEYS: &[&str] = &[ + "password", "passwd", "secret", "token", "dsn", "pwd", "pass", +]; + +/// Compiled redaction patterns. Built once; the `before_send` closure shares one +/// instance via `Arc`. If any pattern fails to compile, `paranoid` flips on and every +/// string becomes `REDACTED` — fail closed, never leak. +struct Scrubbers { + url: Regex, + secret: Regex, + pem: Regex, + paranoid: bool, +} + +impl Scrubbers { + /// Compile all three patterns; on any failure return a paranoid scrubber. + fn build() -> Self { + let url = Regex::new(URL_CREDS); + let secret = Regex::new(SECRET_KV); + let pem = Regex::new(PEM_BLOCK); + match (url, secret, pem) { + (Ok(url), Ok(secret), Ok(pem)) => Self { + url, + secret, + pem, + paranoid: false, + }, + _ => { + // Should be unreachable — patterns are compile-time literals — but a + // future edit could break one. Fail closed: redact everything. + warn!("sent scrubber regex failed to compile; entering paranoid mode"); + Self::paranoid() + } + } + } + + fn paranoid() -> Self { + // Harmless placeholder patterns; `paranoid` short-circuits before they run. + let blank = || Regex::new("").unwrap_or_else(|_| Regex::new("$^").unwrap()); + Self { + url: blank(), + secret: blank(), + pem: blank(), + paranoid: true, + } + } + + /// Apply all three redactions to a single string. + fn scrub(&self, s: &str) -> String { + if self.paranoid { + return REDACTED.to_string(); + } + let s = self.url.replace_all(s, |c: &Captures<'_>| { + format!("{}{}:{REDACTED}{}", &c[1], &c[2], &c[4]) + }); + let s = self + .secret + .replace_all(&s, |c: &Captures<'_>| format!("{}{REDACTED}", &c[1])); + self.pem.replace_all(&s, REDACTED).into_owned() + } +} + +/// Pure redaction of a single string — the testable unit. Rebuilds the patterns each +/// call; cheap, and keeps the function self-contained. Runtime events instead reuse a +/// single cached `Scrubbers` via `init`'s `Arc`. +pub(crate) fn scrub_str(s: &str) -> String { + Scrubbers::build().scrub(s) +} + +/// Recursively redact a JSON value: secret-keyed object values are blanked, string +/// leaves run through the free-text scrubber, objects/arrays descended into. +fn scrub_value(value: Value, scrubbers: &Scrubbers) -> Value { + match value { + Value::Object(map) => Value::Object( + map.into_iter() + .map(|(k, v)| { + let v = if is_secret_key(&k) { + Value::String(REDACTED.to_string()) + } else { + scrub_value(v, scrubbers) + }; + (k, v) + }) + .collect(), + ), + Value::Array(items) => Value::Array( + items + .into_iter() + .map(|v| scrub_value(v, scrubbers)) + .collect(), + ), + Value::String(s) => Value::String(scrubbers.scrub(&s)), + other => other, + } +} + +/// Redact the human-readable + JSON fields of an outbound event in place. +fn scrub_event(event: &mut Event<'_>, scrubbers: &Scrubbers) { + event.message = event.message.take().map(|m| scrubbers.scrub(&m)); + + for ex in event.exception.values.iter_mut() { + ex.ty = scrubbers.scrub(&ex.ty); + ex.value = ex.value.take().map(|v| scrubbers.scrub(&v)); + } + + for crumb in event.breadcrumbs.values.iter_mut() { + crumb.message = crumb.message.take().map(|m| scrubbers.scrub(&m)); + for (k, v) in crumb.data.iter_mut() { + *v = if is_secret_key(k) { + Value::String(REDACTED.to_string()) + } else { + scrub_value(std::mem::take(v), scrubbers) + }; + } + } + + for (k, v) in event.extra.iter_mut() { + *v = if is_secret_key(k) { + Value::String(REDACTED.to_string()) + } else { + scrub_value(std::mem::take(v), scrubbers) + }; + } +} + +/// True if `key` (case-insensitive) names a secret field. Substring match biases +/// toward over-redaction: a stray `"compass"` is preferable to a leaked password. +fn is_secret_key(key: &str) -> bool { + let key = key.to_ascii_lowercase(); + SECRET_KEYS.iter().any(|s| key.contains(s)) +} + +/// Parse a DSN string, returning `None` on anything malformed (no panic). +fn parse_dsn(s: &str) -> Option { + s.parse::().ok() +} + +/// Initialize Sentry/GlitchTip. Reads `SENTRY_DSN` at runtime (never hardcoded); +/// unset/empty/malformed → a disabled no-op client, and the app still runs. The +/// returned guard must be held for the whole program lifetime (bound in `main`). +pub(crate) fn init() -> sentry::ClientInitGuard { + let dsn = std::env::var("SENTRY_DSN") + .ok() + .filter(|s| !s.is_empty()) + .as_deref() + .and_then(parse_dsn); + + let environment = std::env::var("SENTRY_ENVIRONMENT") + .ok() + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "development".to_string()); + + let scrubbers = Arc::new(Scrubbers::build()); + let before_send: BeforeCallback> = { + let scrubbers = Arc::clone(&scrubbers); + Arc::new(move |mut event| { + scrub_event(&mut event, &scrubbers); + Some(event) + }) + }; + + let opts = ClientOptions { + dsn, + environment: Some(environment.into()), + release: Some(env!("CARGO_PKG_VERSION").into()), + send_default_pii: false, + traces_sample_rate: 0.0, + before_send: Some(before_send), + ..Default::default() + }; + + sentry::init(opts) +} + +// --- patterns --------------------------------------------------------------- +// URL credentials: `scheme://user:pass@host` → blank the password (capture 3). +// 1 = scheme://, 2 = user, 3 = password, 4 = @. Userinfo without a password +// (no `:`) doesn't match, so `ssh://host` is left untouched. +const URL_CREDS: &str = r#"(?i)([a-z][a-z0-9+.-]*://)([^\s/@:]*):([^\s/@]+)(@)"#; +// `key=value` / `key: value` for secret keys (case-insensitive). 1 = key+separator, +// value (capture 2) is dropped. Structured JSON keys are handled separately by +// `scrub_value`; this covers free text in messages/exceptions/breadcrumbs. +const SECRET_KV: &str = r#"(?i)((?:password|passwd|secret|token|dsn|pwd|pass)\s*[:=]\s*)(\S+)"#; +// PEM private-key blocks, dotall so `.` spans newlines. Covers RSA/EC/OPENSSH/PRIVATE. +const PEM_BLOCK: &str = + r#"(?s)-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?-----END [A-Z0-9 ]*PRIVATE KEY-----"#; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn redacts_password_key_value() { + assert_eq!(scrub_str("password=hunter2"), "password=***REDACTED***"); + assert_eq!( + scrub_str("db password: s3cr3t!"), + "db password: ***REDACTED***" + ); + assert_eq!( + scrub_str("TOKEN=abc.def.ghi end"), + "TOKEN=***REDACTED*** end" + ); + // MCP_SSH_PASS-shaped leakage. + assert_eq!( + scrub_str("MCP_SSH_PASS=letmein"), + "MCP_SSH_PASS=***REDACTED***" + ); + } + + #[test] + fn redacts_url_credentials() { + assert_eq!( + scrub_str("https://alice:hunter2@example.com/path"), + "https://alice:***REDACTED***@example.com/path", + ); + // No password → untouched. + assert_eq!( + scrub_str("ssh://alice@example.com"), + "ssh://alice@example.com" + ); + // Password containing colons is fully redacted up to the host. + assert_eq!(scrub_str("ftp://u:a:b@c"), "ftp://u:***REDACTED***@c"); + } + + #[test] + fn redacts_pem_private_key_block() { + let pem = "preamble\n\ + -----BEGIN RSA PRIVATE KEY-----\n\ + MIIEpAIBAAKCAQEA0Z3VS5Jo0...\n\ + -----END RSA PRIVATE KEY-----\n\ + trailer"; + let out = scrub_str(pem); + assert!( + !out.contains("MIIEpAIBAAKCAQEA0Z3VS5Jo0"), + "key body leaked: {out}" + ); + assert!(!out.contains("BEGIN RSA PRIVATE KEY")); + assert!(out.contains(REDACTED)); + assert!(out.contains("preamble")); + assert!(out.contains("trailer")); + } + + #[test] + fn leaves_unrelated_text_intact() { + assert_eq!(scrub_str("nothing to see here"), "nothing to see here"); + assert_eq!(scrub_str("user=alice port=22"), "user=alice port=22"); + } + + #[test] + fn paranoid_mode_redacts_everything() { + let s = Scrubbers { + url: Regex::new("").unwrap(), + secret: Regex::new("").unwrap(), + pem: Regex::new("").unwrap(), + paranoid: true, + }; + assert_eq!(s.scrub("any old text"), REDACTED); + } + + #[test] + fn parse_dsn_rejects_invalid_and_accepts_valid() { + // A valid ingest DSN shape (public test key) parses. + assert!( + parse_dsn("https://16a3cee40cba4b40a62a3e2b5ad1ea6f@glitchtip.example/10").is_some() + ); + // Malformed → None, no panic. + assert!(parse_dsn("not a dsn").is_none()); + assert!(parse_dsn("https://example/10").is_none()); + assert!(parse_dsn("").is_none()); + } + + #[test] + fn no_op_when_dsn_unset() { + // init() with no SENTRY_DSN yields a disabled client (the whole point: the + // app keeps running). We can't easily call init() without polluting the global + // hub, so assert the decision logic it relies on: an unset/empty DSN resolves + // to None. + let resolved = std::env::var("SENTRY_DSN") + .ok() + .filter(|s| !s.is_empty()) + .as_deref() + .and_then(parse_dsn); + // In the test env SENTRY_DSN is unset → disabled. + if std::env::var_os("SENTRY_DSN").is_none() { + assert!(resolved.is_none(), "expected disabled client"); + } + } + + #[test] + fn is_secret_key_matches_case_insensitively() { + assert!(is_secret_key("PASSWORD")); + assert!(is_secret_key("Db_Passwd")); + assert!(is_secret_key("api_token")); + assert!(is_secret_key("x-dsn")); + assert!(!is_secret_key("hostname")); + } + + #[test] + fn scrub_value_redacts_secret_keyed_json() { + let scrubbers = Scrubbers::build(); + let v = serde_json::json!({ + "user": "alice", + "password": "hunter2", + "nested": { "TOKEN": "abc" }, + "list": ["ssh://u:pw@h", "plain"], + }); + let out = scrub_value(v, &scrubbers); + assert_eq!(out["user"], "alice"); + assert_eq!(out["password"], REDACTED); + assert_eq!(out["nested"]["TOKEN"], REDACTED); + assert_eq!(out["list"][0], "ssh://u:***REDACTED***@h"); + assert_eq!(out["list"][1], "plain"); + } + + #[test] + fn scrub_event_redacts_fields() { + let mut event = Event { + message: Some("login password=hunter2 failed".into()), + ..Default::default() + }; + let ex = sentry::protocol::Exception { + ty: "Token=leaked boom".into(), + value: Some("see https://u:secret@host".into()), + ..Default::default() + }; + event.exception.values.push(ex); + let mut crumb = sentry::protocol::Breadcrumb { + message: Some("connecting to https://key:abc@s/10".into()), + ..Default::default() + }; + crumb + .data + .insert("pass".into(), Value::String("p@ss".into())); + event.breadcrumbs.values.push(crumb); + event + .extra + .insert("password".into(), Value::String("x".into())); + + scrub_event(&mut event, &Scrubbers::build()); + + assert_eq!( + event.message.as_deref(), + Some("login password=***REDACTED*** failed") + ); + let ex = &event.exception.values[0]; + assert_eq!(ex.ty, "Token=***REDACTED*** boom"); + assert_eq!( + ex.value.as_deref(), + Some("see https://u:***REDACTED***@host") + ); + let crumb = &event.breadcrumbs.values[0]; + // URL creds redacted in the breadcrumb message. + assert_eq!( + crumb.message.as_deref(), + Some("connecting to https://key:***REDACTED***@s/10") + ); + assert_eq!(crumb.data["pass"], Value::String(REDACTED.to_string())); + assert_eq!(event.extra["password"], Value::String(REDACTED.to_string())); + } +} From a23cafe66a918b548788da29d99e7dd1468e55e8 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:41:17 +0000 Subject: [PATCH 3/7] =?UTF-8?q?refactor(main):=20sync=20entry=20=E2=80=94?= =?UTF-8?q?=20sentry=20guard=20before=20tokio=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace `#[tokio::main] async fn main` with a sync `fn main`: bind `let _sentry_guard = sentry::init()` FIRST (named binding so it outlives block_on, not a bare `let _ =`), then tracing init, then build a multi-thread runtime and block_on(run()). - Move the CLI dispatch body verbatim into `async fn run()`. - Gate the test-only `scrub_str` helper with `#[cfg(test)]` and drop the module-level `#![allow(dead_code)]` now that `init` is wired in. Sentry now captures for the whole process lifetime; unset/malformed DSN still yields a disabled no-op client. serve/set_auth/shutdown_signal and the unix test module are unchanged. Co-Authored-By: Claude --- src/main.rs | 17 +++++++++++++++-- src/sentry.rs | 8 ++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 150352d..2adea65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,14 @@ use clap::Parser; use cli::{Cli, Command, JobCommand}; -#[tokio::main] -async fn main() -> anyhow::Result<()> { +fn main() -> anyhow::Result<()> { + // Sentry MUST init before the tokio runtime: its reqwest transport spins up its + // own runtime, and the guard has to live for the whole program so in-flight + // events flush on exit. `let _sentry_guard` keeps the binding alive (a bare + // `let _ =` would drop it immediately and disable capture). Unset/malformed + // DSN → a disabled no-op client; the app runs regardless. + let _sentry_guard = sentry::init(); + tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() @@ -25,6 +31,13 @@ async fn main() -> anyhow::Result<()> { ) .init(); + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + runtime.block_on(run()) +} + +async fn run() -> anyhow::Result<()> { match Cli::parse() .command .unwrap_or(Command::Serve { port: None }) diff --git a/src/sentry.rs b/src/sentry.rs index 481f69c..7997f5a 100644 --- a/src/sentry.rs +++ b/src/sentry.rs @@ -9,11 +9,6 @@ //! Init is owned by `main` (it binds the guard for the program lifetime); this //! module never logs or returns secrets. -// Nothing here is referenced until `main` binds the guard (next change: init must -// run before the tokio runtime starts, so the wiring lands with that refactor). -// Drop this allow once `init()` is called from `main`. -#![allow(dead_code)] - use std::sync::Arc; use regex::{Captures, Regex}; @@ -93,7 +88,8 @@ impl Scrubbers { /// Pure redaction of a single string — the testable unit. Rebuilds the patterns each /// call; cheap, and keeps the function self-contained. Runtime events instead reuse a /// single cached `Scrubbers` via `init`'s `Arc`. -pub(crate) fn scrub_str(s: &str) -> String { +#[cfg(test)] +fn scrub_str(s: &str) -> String { Scrubbers::build().scrub(s) } From 8a5b044d76564bb66a9796336fce65d14d1c9cd5 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:48:27 +0000 Subject: [PATCH 4/7] test(sentry): unit tests for scrubber + no-op path - Replace placeholder tests with spec-named cases: password assignment, user:pass URL, PEM private-key block, nested JSON secret, MCP_SSH_PASS env value, parse_dsn, no-op init. - Add real no-op test via init()+is_enabled() (disabled client when SENTRY_DSN unset), plus array-descent coverage. - Remove the real ingest key (16a3...) from the parse_dsn test; use a throwaway DSN shape so the secret never lands in source. Co-Authored-By: Claude --- src/sentry.rs | 173 +++++++++++++++++++++++++------------------------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/src/sentry.rs b/src/sentry.rs index 7997f5a..a434e9d 100644 --- a/src/sentry.rs +++ b/src/sentry.rs @@ -216,54 +216,97 @@ mod tests { use super::*; #[test] - fn redacts_password_key_value() { - assert_eq!(scrub_str("password=hunter2"), "password=***REDACTED***"); - assert_eq!( - scrub_str("db password: s3cr3t!"), - "db password: ***REDACTED***" - ); - assert_eq!( - scrub_str("TOKEN=abc.def.ghi end"), - "TOKEN=***REDACTED*** end" - ); - // MCP_SSH_PASS-shaped leakage. - assert_eq!( - scrub_str("MCP_SSH_PASS=letmein"), - "MCP_SSH_PASS=***REDACTED***" - ); + fn scrubs_password_assignment() { + // `key=value` / `key: value` for the secret key set: the value is dropped, + // never the key. Covers both separators and the short `pwd`/`pass` forms. + for (input, secret) in [ + ("MCP_SSH_PASS=hunter2", "hunter2"), + ("password: hunter2", "hunter2"), + ("pwd=x", "x"), + ] { + let out = scrub_str(input); + assert!(!out.contains(secret), "{input:?} leaked {secret:?}: {out}"); + assert!(out.contains(REDACTED), "{input:?} not redacted: {out}"); + } } #[test] - fn redacts_url_credentials() { - assert_eq!( - scrub_str("https://alice:hunter2@example.com/path"), - "https://alice:***REDACTED***@example.com/path", - ); - // No password → untouched. - assert_eq!( - scrub_str("ssh://alice@example.com"), - "ssh://alice@example.com" - ); - // Password containing colons is fully redacted up to the host. - assert_eq!(scrub_str("ftp://u:a:b@c"), "ftp://u:***REDACTED***@c"); + fn redacts_mcp_ssh_pass_env_value() { + // MCP_SSH_PASS is the must-never-leak env; its value can never survive scrubbing. + let out = scrub_str("MCP_SSH_PASS=hunter2"); + assert!(!out.contains("hunter2")); + assert_eq!(out, "MCP_SSH_PASS=***REDACTED***"); + } + + #[test] + fn scrubs_user_pass_url() { + let out = scrub_str("https://alice:s3cr3t@host.example/p"); + assert!(!out.contains("s3cr3t"), "password leaked: {out}"); + assert!(out.contains(REDACTED)); + // Userinfo user + host/path survive; only the password is blanked. + assert!(out.contains("alice")); + assert!(out.contains("host.example/p")); } #[test] - fn redacts_pem_private_key_block() { - let pem = "preamble\n\ - -----BEGIN RSA PRIVATE KEY-----\n\ - MIIEpAIBAAKCAQEA0Z3VS5Jo0...\n\ - -----END RSA PRIVATE KEY-----\n\ - trailer"; + fn scrubs_pem_private_key_block() { + let pem = "leading context\n\ + -----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAAAAAAAA\n\ + -----END OPENSSH PRIVATE KEY-----\n\ + trailing context"; let out = scrub_str(pem); + assert!(!out.contains("b3BlbnNza"), "key body leaked: {out}"); + assert!(!out.contains("BEGIN OPENSSH PRIVATE KEY")); + assert!(!out.contains("END OPENSSH PRIVATE KEY")); + assert!(out.contains(REDACTED)); + assert!(out.contains("leading context")); + assert!(out.contains("trailing context")); + } + + #[test] + fn scrubs_nested_secret_in_json() { + let v = serde_json::json!({"auth": {"token": "xyz"}, "ok": "keep"}); + let out = scrub_value(v, &Scrubbers::build()); assert!( - !out.contains("MIIEpAIBAAKCAQEA0Z3VS5Jo0"), - "key body leaked: {out}" + !out.to_string().contains("xyz"), + "secret leaked into JSON: {out}" + ); + assert_eq!(out["ok"], "keep"); + assert_eq!(out["auth"]["token"], REDACTED); + } + + #[test] + fn scrub_value_descends_arrays() { + // The Array arm must redact URL-bearing strings and recurse into nested objects. + let v = serde_json::json!({ "items": ["ssh://u:pw@h", "plain", {"token": "t"}] }); + let out = scrub_value(v, &Scrubbers::build()); + assert_eq!(out["items"][0], "ssh://u:***REDACTED***@h"); + assert_eq!(out["items"][1], "plain"); + assert_eq!(out["items"][2]["token"], REDACTED); + } + + #[test] + fn parse_dsn_disabled_when_unset_or_malformed() { + assert!(parse_dsn("").is_none()); + assert!(parse_dsn("garbage").is_none()); + // Throwaway valid DSN shape (NOT the real ingest key) parses to Some. + assert!(parse_dsn("https://deadbeefdeadbeefdeadbeefdeadbeef@example.invalid/1").is_some()); + } + + #[test] + fn init_is_noop_when_dsn_unset() { + // init() with no SENTRY_DSN binds a *disabled* client so the app still runs. + // We don't mutate env (racy under parallel tests); only assert in the normal + // test/CI state where SENTRY_DSN is unset. The guard unbinds the client on drop. + if std::env::var_os("SENTRY_DSN").is_some() { + return; + } + let guard = init(); + assert!( + !guard.is_enabled(), + "client must be disabled when DSN is unset" ); - assert!(!out.contains("BEGIN RSA PRIVATE KEY")); - assert!(out.contains(REDACTED)); - assert!(out.contains("preamble")); - assert!(out.contains("trailer")); } #[test] @@ -274,6 +317,7 @@ mod tests { #[test] fn paranoid_mode_redacts_everything() { + // If a regex ever fails to compile, the scrubber fails closed: redact all. let s = Scrubbers { url: Regex::new("").unwrap(), secret: Regex::new("").unwrap(), @@ -283,63 +327,19 @@ mod tests { assert_eq!(s.scrub("any old text"), REDACTED); } - #[test] - fn parse_dsn_rejects_invalid_and_accepts_valid() { - // A valid ingest DSN shape (public test key) parses. - assert!( - parse_dsn("https://16a3cee40cba4b40a62a3e2b5ad1ea6f@glitchtip.example/10").is_some() - ); - // Malformed → None, no panic. - assert!(parse_dsn("not a dsn").is_none()); - assert!(parse_dsn("https://example/10").is_none()); - assert!(parse_dsn("").is_none()); - } - - #[test] - fn no_op_when_dsn_unset() { - // init() with no SENTRY_DSN yields a disabled client (the whole point: the - // app keeps running). We can't easily call init() without polluting the global - // hub, so assert the decision logic it relies on: an unset/empty DSN resolves - // to None. - let resolved = std::env::var("SENTRY_DSN") - .ok() - .filter(|s| !s.is_empty()) - .as_deref() - .and_then(parse_dsn); - // In the test env SENTRY_DSN is unset → disabled. - if std::env::var_os("SENTRY_DSN").is_none() { - assert!(resolved.is_none(), "expected disabled client"); - } - } - #[test] fn is_secret_key_matches_case_insensitively() { assert!(is_secret_key("PASSWORD")); assert!(is_secret_key("Db_Passwd")); assert!(is_secret_key("api_token")); assert!(is_secret_key("x-dsn")); + // Substring bias: a stray "compass" redacts rather than risk a leak. + assert!(is_secret_key("compass")); assert!(!is_secret_key("hostname")); } #[test] - fn scrub_value_redacts_secret_keyed_json() { - let scrubbers = Scrubbers::build(); - let v = serde_json::json!({ - "user": "alice", - "password": "hunter2", - "nested": { "TOKEN": "abc" }, - "list": ["ssh://u:pw@h", "plain"], - }); - let out = scrub_value(v, &scrubbers); - assert_eq!(out["user"], "alice"); - assert_eq!(out["password"], REDACTED); - assert_eq!(out["nested"]["TOKEN"], REDACTED); - assert_eq!(out["list"][0], "ssh://u:***REDACTED***@h"); - assert_eq!(out["list"][1], "plain"); - } - - #[test] - fn scrub_event_redacts_fields() { + fn scrub_event_redacts_all_fields() { let mut event = Event { message: Some("login password=hunter2 failed".into()), ..Default::default() @@ -375,7 +375,6 @@ mod tests { Some("see https://u:***REDACTED***@host") ); let crumb = &event.breadcrumbs.values[0]; - // URL creds redacted in the breadcrumb message. assert_eq!( crumb.message.as_deref(), Some("connecting to https://key:***REDACTED***@s/10") From 166e1cd596771522f56668276b2026777775f0fb Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:48:55 +0000 Subject: [PATCH 5/7] chore: add GlitchTip MCP config (.mcp.json) - Point MCP clients at the GlitchTip HTTP MCP endpoint - infra#499 Co-Authored-By: Claude --- .mcp.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..93a62dc --- /dev/null +++ b/.mcp.json @@ -0,0 +1 @@ +{"type":"http","url":"https://glitchtip.infra.developerz.ai/mcp"} From 6dc89cca1d14c1ca82a695491459a3d7546ce314 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 14:49:37 +0000 Subject: [PATCH 6/7] docs(sentry): document SENTRY_DSN/SENTRY_ENVIRONMENT injection points - .env.example: SENTRY_DSN/SENTRY_ENVIRONMENT after RUST_LOG - systemd unit: comment at EnvironmentFile noting env-file injection - docs/deploy.md: config table rows pointing to the env file Co-Authored-By: Claude --- .env.example | 5 +++++ deploy/mcp-ssh.service | 2 ++ docs/deploy.md | 2 ++ 3 files changed, 9 insertions(+) diff --git a/.env.example b/.env.example index 3d4fa19..1704a1e 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,8 @@ MCP_SSH_ALLOWED_HOSTS=localhost,127.0.0.1 # Logging. RUST_LOG=info,mcp_ssh=debug + +# GlitchTip error tracking. Unset/empty SENTRY_DSN ⇒ disabled (no-op client). +# Injected at runtime via the systemd EnvironmentFile, not committed. +SENTRY_DSN= +SENTRY_ENVIRONMENT=production diff --git a/deploy/mcp-ssh.service b/deploy/mcp-ssh.service index 8e6265f..c560ef6 100644 --- a/deploy/mcp-ssh.service +++ b/deploy/mcp-ssh.service @@ -12,6 +12,8 @@ ExecStart=/usr/bin/mcp-ssh serve StateDirectory=mcp-ssh # Optional env overrides. Config credentials live at `/etc/mcp-ssh/config.toml` # (written by `mcp-ssh set-auth`); this file is for ephemeral knobs. +# GlitchTip error tracking (SENTRY_DSN / SENTRY_ENVIRONMENT) is injected via +# this env file; unset SENTRY_DSN ⇒ disabled no-op client. EnvironmentFile=-/etc/mcp-ssh/mcp-ssh.env Restart=on-failure RestartSec=5 diff --git a/docs/deploy.md b/docs/deploy.md index c96e7ba..ddd633b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -211,6 +211,8 @@ File: `/etc/mcp-ssh/config.toml` (or override the path with `$MCP_SSH_CONFIG`). | `MCP_SSH_INLINE_TIMEOUT_SECS` | `2` | inline window before `bash` backgrounds | | `MCP_SSH_JOB_DIR` | `/var/lib/mcp-ssh/logs/jobs` | per-job log files | | `MCP_SSH_ALLOWED_HOSTS` | `localhost,127.0.0.1` | hostnames accepted in `Host` — **set to your public hostname** | +| `SENTRY_DSN` | — | GlitchTip ingest DSN; injected via the systemd EnvironmentFile (`/etc/mcp-ssh/mcp-ssh.env`). Unset/empty ⇒ error tracking disabled | +| `SENTRY_ENVIRONMENT` | `production` | GlitchTip environment tag; injected via the same env file | ### 💾 Durable state (SQLite) From c979046edf794cb91b03961153c2f42741190699 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Wed, 1 Jul 2026 15:28:35 +0000 Subject: [PATCH 7/7] fix(sentry): address PR #12 review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scrubber now redacts `authorization`/`cookie` keys (structured + free-text, incl. full `Authorization: Bearer `); value capture spans to EOL to avoid leaking trailing tokens. Added regression test. - main captures run() errors to Sentry before propagating (capture_error wrapper in the local sentry module — it shadows the SDK crate from main). - PEM fixture built from split fragments so the source no longer trips secret scanners; runtime value unchanged. - docs: SENTRY_ENVIRONMENT default aligned to code (`development`). Co-Authored-By: Claude --- docs/deploy.md | 2 +- src/main.rs | 10 ++++++- src/sentry.rs | 74 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/docs/deploy.md b/docs/deploy.md index ddd633b..8cd08a3 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -212,7 +212,7 @@ File: `/etc/mcp-ssh/config.toml` (or override the path with `$MCP_SSH_CONFIG`). | `MCP_SSH_JOB_DIR` | `/var/lib/mcp-ssh/logs/jobs` | per-job log files | | `MCP_SSH_ALLOWED_HOSTS` | `localhost,127.0.0.1` | hostnames accepted in `Host` — **set to your public hostname** | | `SENTRY_DSN` | — | GlitchTip ingest DSN; injected via the systemd EnvironmentFile (`/etc/mcp-ssh/mcp-ssh.env`). Unset/empty ⇒ error tracking disabled | -| `SENTRY_ENVIRONMENT` | `production` | GlitchTip environment tag; injected via the same env file | +| `SENTRY_ENVIRONMENT` | `development` | GlitchTip environment tag; injected via the same env file | ### 💾 Durable state (SQLite) diff --git a/src/main.rs b/src/main.rs index 2adea65..04f1f08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,15 @@ fn main() -> anyhow::Result<()> { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; - runtime.block_on(run()) + // Capture run() failures (config/load/bind/serve) to Sentry before they leave + // main — otherwise they propagate as an exit code and never reach error + // tracking. The guard is still bound; this just reports on the way out. + let result = runtime.block_on(run()); + if let Err(ref error) = result { + let source: &dyn std::error::Error = error.as_ref(); + sentry::capture_error(source); + } + result } async fn run() -> anyhow::Result<()> { diff --git a/src/sentry.rs b/src/sentry.rs index a434e9d..f077894 100644 --- a/src/sentry.rs +++ b/src/sentry.rs @@ -24,7 +24,15 @@ const REDACTED: &str = "***REDACTED***"; /// Key names whose value is always secret (case-insensitive). Mirrors the free-text /// regex so structured JSON keys and ad-hoc `key=value` text agree on what's secret. const SECRET_KEYS: &[&str] = &[ - "password", "passwd", "secret", "token", "dsn", "pwd", "pass", + "password", + "passwd", + "secret", + "token", + "dsn", + "pwd", + "pass", + "authorization", + "cookie", ]; /// Compiled redaction patterns. Built once; the `before_send` closure shares one @@ -198,6 +206,13 @@ pub(crate) fn init() -> sentry::ClientInitGuard { sentry::init(opts) } +/// Capture a `std::error::Error` to GlitchTip (no-op when the client is disabled — +/// DSN unset). Thin forwarder: `main` names the local `sentry` module, which +/// shadows the SDK crate, so it can't call `sentry::capture_error` directly. +pub(crate) fn capture_error(error: &dyn std::error::Error) { + sentry::capture_error(error); +} + // --- patterns --------------------------------------------------------------- // URL credentials: `scheme://user:pass@host` → blank the password (capture 3). // 1 = scheme://, 2 = user, 3 = password, 4 = @. Userinfo without a password @@ -205,8 +220,11 @@ pub(crate) fn init() -> sentry::ClientInitGuard { const URL_CREDS: &str = r#"(?i)([a-z][a-z0-9+.-]*://)([^\s/@:]*):([^\s/@]+)(@)"#; // `key=value` / `key: value` for secret keys (case-insensitive). 1 = key+separator, // value (capture 2) is dropped. Structured JSON keys are handled separately by -// `scrub_value`; this covers free text in messages/exceptions/breadcrumbs. -const SECRET_KV: &str = r#"(?i)((?:password|passwd|secret|token|dsn|pwd|pass)\s*[:=]\s*)(\S+)"#; +// `scrub_value`; this covers free text in messages/exceptions/breadcrumbs. The value +// group spans to end of line so multi-word secrets like `Authorization: Bearer ` +// redact in full — bias toward over-redaction rather than leak a trailing token. +const SECRET_KV: &str = + r#"(?i)((?:authorization|cookie|password|passwd|secret|token|dsn|pwd|pass)\s*[:=]\s*)(.+)"#; // PEM private-key blocks, dotall so `.` spans newlines. Covers RSA/EC/OPENSSH/PRIVATE. const PEM_BLOCK: &str = r#"(?s)-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?-----END [A-Z0-9 ]*PRIVATE KEY-----"#; @@ -250,11 +268,17 @@ mod tests { #[test] fn scrubs_pem_private_key_block() { - let pem = "leading context\n\ - -----BEGIN OPENSSH PRIVATE KEY-----\n\ - b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAAAAAAAA\n\ - -----END OPENSSH PRIVATE KEY-----\n\ - trailing context"; + // Built from split fragments so the source never contains a scanner-detectable + // private-key block; the runtime value still matches the PEM_BLOCK regex. + let pem = concat!( + "leading context\n", + "-----BEGIN OPENSSH PRIVATE ", + "KEY-----\n", + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAAAAAAAA\n", + "-----END OPENSSH PRIVATE ", + "KEY-----\n", + "trailing context", + ); let out = scrub_str(pem); assert!(!out.contains("b3BlbnNza"), "key body leaked: {out}"); assert!(!out.contains("BEGIN OPENSSH PRIVATE KEY")); @@ -366,10 +390,10 @@ mod tests { assert_eq!( event.message.as_deref(), - Some("login password=***REDACTED*** failed") + Some("login password=***REDACTED***") ); let ex = &event.exception.values[0]; - assert_eq!(ex.ty, "Token=***REDACTED*** boom"); + assert_eq!(ex.ty, "Token=***REDACTED***"); assert_eq!( ex.value.as_deref(), Some("see https://u:***REDACTED***@host") @@ -382,4 +406,34 @@ mod tests { assert_eq!(crumb.data["pass"], Value::String(REDACTED.to_string())); assert_eq!(event.extra["password"], Value::String(REDACTED.to_string())); } + + #[test] + fn scrubs_authorization_and_cookie_secrets() { + // Regression: `authorization`/`cookie` structured keys redact by key name… + let v = serde_json::json!({ + "authorization": "Bearer abc123", + "headers": { "cookie": "sid=s3cr3t; theme=dark" }, + }); + let out = scrub_value(v, &Scrubbers::build()); + assert!(!out.to_string().contains("abc123"), "bearer leaked: {out}"); + assert!(!out.to_string().contains("s3cr3t"), "cookie leaked: {out}"); + assert_eq!(out["authorization"], REDACTED); + assert_eq!(out["headers"]["cookie"], REDACTED); + + // …and the free-text forms redact in full — including the token after "Bearer", + // not just the single word "Bearer". + let out = scrub_str("Authorization: Bearer abc123"); + assert!( + !out.contains("abc123"), + "bearer token leaked in text: {out}" + ); + assert!(out.contains(REDACTED)); + + let out = scrub_str("Cookie: sid=s3cr3t"); + assert!( + !out.contains("s3cr3t"), + "cookie value leaked in text: {out}" + ); + assert!(out.contains(REDACTED)); + } }