diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 8fe743684c..100b5ba964 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -220,6 +220,7 @@ jobs: cargo fmt --manifest-path e2e/rust/Cargo.toml --all -- --check cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all -- --check cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check + cargo fmt --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml --all -- --check cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all -- --check cargo-lockfiles: @@ -276,6 +277,7 @@ jobs: cargo clippy --locked --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings + cargo clippy --locked --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/isolation-backend-v8/Cargo.toml --all-targets -- -D warnings rust-test: @@ -325,6 +327,7 @@ jobs: run: | cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml + cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml rust-postgres-test: name: Rust PostgreSQL tests diff --git a/docs/extensibility/supervisor-middleware/operations.mdx b/docs/extensibility/supervisor-middleware/operations.mdx index efb3649ef1..1b4e439b14 100644 --- a/docs/extensibility/supervisor-middleware/operations.mdx +++ b/docs/extensibility/supervisor-middleware/operations.mdx @@ -61,6 +61,8 @@ OpenShell applies middleware changes before it injects provider credentials. Mid Request output streams to the external service while the sandbox uploads. OpenShell instead buffers output when a stage selects BUFFERED, the route needs SigV4 signing or body credential rewriting, or the request uses the forward proxy, HTTP/1.0, or `Upgrade`. JSON-RPC, MCP, and GraphQL endpoints also buffer output so OpenShell can check the replaced body against policy. Buffered output can be up to 10 MiB for SigV4 signing, 256 KiB for body credential rewriting, and 4 MiB otherwise. The preflight reports this limit to STREAM stages as `limits.max_output_body_bytes`. Larger output fails with `body_over_limit`. +A STREAM stage can consume all input before sending `output_start`. The [Git-signing example](https://github.com/pimlock/OpenShell/tree/4359-git-signing-validation/pimlock/examples/supervisor-middleware-git-signing) uses this order to rewrite and sign a complete push without putting a signing key in the sandbox. It stores the push in a temporary file, then declares the replacement length and returns output chunks. Commit rewriting changes object IDs, so this prototype requires users to reconcile their local branches after pushing. + ### Responses Your service receives the response status, headers, and request context. It can return `continue_without_body` or choose a body mode with `inspect`. diff --git a/examples/supervisor-middleware-git-signing/Cargo.lock b/examples/supervisor-middleware-git-signing/Cargo.lock new file mode 100644 index 0000000000..6440bc19ad --- /dev/null +++ b/examples/supervisor-middleware-git-signing/Cargo.lock @@ -0,0 +1,3999 @@ +# This file is automatically @generated by Cargo. +# 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[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 = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" +dependencies = [ + "serde", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "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 = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "headers" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2700e3fe555c3310aa7286cac6167449f2c87e0eb58769c9208a1c58a1d106" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.7", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-http-proxy" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonpath-rust" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.17", + "js-sys", + "pem 3.0.6", + "serde", + "serde_json", + "signature", + "simple_asn1", + "zeroize", +] + +[[package]] +name = "k8s-openapi" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5" +dependencies = [ + "base64 0.22.1", + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "kube" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4eb20010536b48abe97fec37d23d43069bcbe9686adcf9932202327bc5ca6e" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "either", + "futures", + "home", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem 3.0.6", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff0d0793db58e70ca6d689489183816cb3aa481673e7433dc618cf7e8007c675" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror", +] + +[[package]] +name = "kube-derive" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c562f58dc9f7ca5feac8a6ee5850ca221edd6f04ce0dd2ee873202a88cd494c9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "kube-runtime" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f34cfab9b4bd8633062e0e85edb81df23cb09f159f2e31c60b069ae826ffdc" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "async-trait", + "backon", + "educe", + "futures", + "hashbrown 0.15.5", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noyalib" +version = "0.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "libm", + "memchr", + "rustc-hash", + "serde_core", + "smallvec", +] + +[[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-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openshell-core" +version = "0.0.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "glob", + "ipnet", + "miette", + "nix", + "openshell-crypto", + "openshell-extension-core", + "openshell-policy-schema", + "prost", + "prost-types", + "protoc-bin-vendored", + "rustix", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tonic-types", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "openshell-crypto" +version = "0.0.0" +dependencies = [ + "aws-lc-rs", + "jsonwebtoken", + "kube", + "pem 3.0.6", + "rcgen", + "rustls", + "serde", + "sqlx", + "thiserror", + "tonic", + "x509-parser", +] + +[[package]] +name = "openshell-extension-core" +version = "0.0.0" +dependencies = [ + "hyper-util", + "serde", + "thiserror", + "tokio", + "tonic", + "tower", +] + +[[package]] +name = "openshell-policy-schema" +version = "0.0.0" +dependencies = [ + "miette", + "noyalib", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd47a460a9cbc3b3429c7e22bf0d76653e779319bae45a499ca010c9a990bb23" +dependencies = [ + "memchr", + "psm", + "stacker", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203e46697708f7519ab1608393cf89239cafb46b8e6d08a8ffe359359b11ef44" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e84ae6340a458d3d6cfc73cf328cf3cfe90b2484cd0de25eb66aa954615ed4d5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "pest_meta" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b57f92f32b3dbce6598fd8f5cdf1f06a6072570b8103fa21728fa9036923e710" +dependencies = [ + "pest", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8760a25b6ff9c620324822737e468478fa092234190d2e449760344354896ed9" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fa2624782ca04cd44f51554566717377acd240e4c0016d757dd74fccc9324f" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2417e9817fa237dab803ad4dda7357a111656e242959cc6b8f9a1a583367d42" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d189c34636356a46a7ed3188233dc8a88c431278cc54d4a19b096a2d270e985" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171e39f1e846e5f322ced1ac3b8d4cd3a3833ca24b6e5d58b3632574fe6204fa" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873cdcc097593432086661aa432b8078f1cd87bfb02847c332e98ae2c119e966" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb72df001783b8297847fe8f5f874ee400fd742c843d60583e8c23d96977c7f" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04652167eca899dda05f32f5481adeaf25c623a98ce2fc146a001cc59a2add7" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263a3f48f01e7309e857138bd47f785585b4a005e8e56c6d2824ce91195999c3" + +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bbb29c624230c4bd1047bbdb2aa47e41c860e9665ce62ba9504eebe91bf867" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "aws-lc-rs", + "pem 4.0.0", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "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 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[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 = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.1", + "sqlx-core", + "thiserror", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.1", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[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 = "supervisor-middleware-git-signing" +version = "0.0.0" +dependencies = [ + "clap", + "jsonwebtoken", + "openshell-core", + "openshell-extension-core", + "prost-types", + "rcgen", + "tempfile", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[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 = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +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 = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "base64 0.22.1", + "bitflags", + "bytes", + "http", + "http-body", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "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 2.0.119", +] + +[[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 = "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 = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure 0.14.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/supervisor-middleware-git-signing/Cargo.toml b/examples/supervisor-middleware-git-signing/Cargo.toml new file mode 100644 index 0000000000..df972d4f0f --- /dev/null +++ b/examples/supervisor-middleware-git-signing/Cargo.toml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[workspace] + +[package] +name = "supervisor-middleware-git-signing" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +publish = false + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +jsonwebtoken = { version = "10", features = ["aws_lc_rs"] } +openshell-core = { path = "../../crates/openshell-core", default-features = false } +openshell-extension-core = { path = "../../crates/openshell-extension-core" } +prost-types = "0.14" +tempfile = "3" +tokio = { version = "1.43", features = ["fs", "io-util", "macros", "rt-multi-thread"] } +tokio-stream = { version = "0.1", features = ["net"] } +tonic = { version = "0.14", features = ["transport"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[lints.rust] +unsafe_code = "forbid" + +[dev-dependencies] +rcgen = "0.14" diff --git a/examples/supervisor-middleware-git-signing/README.md b/examples/supervisor-middleware-git-signing/README.md new file mode 100644 index 0000000000..e1146a5c24 --- /dev/null +++ b/examples/supervisor-middleware-git-signing/README.md @@ -0,0 +1,102 @@ + + +# Git commit signing supervisor middleware prototype + +This example tests whether an operator-run OpenShell supervisor middleware can sign commits without mounting a signing key into the sandbox. It uses `HttpSessionMiddleware.EvaluateRequest` to intercept Git smart-HTTP `git-receive-pack` requests after network policy admission and before provider credential injection. The service selects `STREAM`, stores input chunks in an unlinked temporary file until `input_end`, rewrites the pushed commit objects with SSH signatures, and returns `output_start`, bounded output chunks, and `finish`. It sends no output until signing succeeds. + +The sandbox sees neither the private key nor the signature operation. Its Git client creates ordinary unsigned commits and pushes over HTTPS. + +## What the prototype does + +For each direct branch update in a push, the service: + +1. Derives a bounded `github.com//.git` upstream URL from the policy-admitted request target. +2. Shallow-fetches the upstream default branch and updated branch tips into a temporary bare repository. This supplies bases omitted from normal thin pushes. +3. Decodes the receive-pack command pkt-lines and packfile, then walks only commits that are not already reachable from the fetched upstream refs. +4. Removes any existing commit signature and signs the exact rewritten commit payload with `ssh-keygen -Y sign -n git`. +5. Rewrites parent object IDs, creates a replacement thin pack, and substitutes the new branch-tip object ID in the receive-pack command. +6. Returns the modified body to the supervisor, which forwards it with the sandbox's normal GitHub credential. + +The signing key is a service startup argument, not policy-controlled middleware configuration. A sandbox cannot select an arbitrary host file to sign with. + +## Build and test + +The integration test constructs a two-commit push larger than the 4 MiB BUFFERED limit, transforms it, sends the replacement request through Git's real `receive-pack --stateless-rpc`, checks the updated branch tip, and verifies both SSH signatures with `git verify-commit`. + +```shell +cargo test --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml +``` + +The example requires `git` and `ssh-keygen` on the host. + +## Run the service + +Use an SSH key that is also registered as a signing key with the Git forge. GitHub distinguishes signing keys from authentication keys even when the same public key material is used. + +The service requires TLS and verifies every OpenShell caller with an exact-audience extension JWT. Provision a service certificate and private key whose DNS name matches the middleware endpoint, the issuing CA certificate for the gateway registration, and the Ed25519 public key configured in `[openshell.gateway.gateway_jwt]`. + +```shell +cargo run \ + --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml \ + -- \ + --bind 0.0.0.0:50051 \ + --signing-key /run/secrets/git-signing-key \ + --tls-cert /run/secrets/git-signer.pem \ + --tls-key /run/secrets/git-signer-key.pem \ + --extension-public-key /run/secrets/openshell-extension-public.pem \ + --expected-gateway-id openshell \ + --audience urn:openshell:extension:middleware:local-git-signer \ + --max-concurrent-signings 2 \ + --signing-timeout-seconds 25 +``` + +Register the local service in `gateway.toml`. A container-backed gateway can reach a host service through `host.openshell.internal`; a host-native gateway can use `127.0.0.1`. + +```toml +[[openshell.supervisor.middleware]] +name = "local-git-signer" +grpc_endpoint = "https://host.openshell.internal:50051" +tls_ca_cert_path = "/etc/openshell/certs/git-signer-ca.pem" +audience = "urn:openshell:extension:middleware:local-git-signer" +max_payload_bytes = 65536 +timeout = "30s" + +[openshell.gateway.gateway_jwt] +signing_key_path = "/etc/openshell/jwt/signing.pem" +public_key_path = "/etc/openshell/jwt/public.pem" +kid_path = "/etc/openshell/jwt/kid" +gateway_id = "openshell" +ttl_secs = 3600 +``` + +The `public_key_path` file must be the same key supplied to the service with `--extension-public-key`. The registration audience must exactly match `--audience`, and `gateway_id` must exactly match `--expected-gateway-id`. The service authorizes gateway callers for discovery and configuration validation and sandbox supervisor callers for request evaluation. + +Set `max_payload_bytes` to 65536 as shown for full-size stream chunks. A lower positive value uses smaller output chunks; the value does not cap the complete push. The service caps each input and replacement push at 256 MiB, or the route's lower `max_output_body_bytes` limit. Keep the registration timeout above the service's signing timeout so OpenShell can receive either the signed output or a controlled failure. + +Restart the gateway after adding the static registration, then replace `` and `` in [policy.yaml](policy.yaml) and create a sandbox with that policy. Keep the endpoint on `fail_closed`: reject a push rather than forwarding it unsigned when signing fails. + +Place the signer after other middleware that changes request bodies. A later body change can invalidate the rewritten Git pack or its signatures. + +The service skips unrelated GitHub HTTP requests during preflight. It inspects only `POST` requests whose path ends in `/git-receive-pack` and whose content type is `application/x-git-receive-pack-request`. This example accepts only validated HTTPS targets on `github.com:443` with a two-segment repository path. + +The signer implements `SupervisorMiddleware` for discovery and configuration validation, and serves `HttpSessionMiddleware` alongside it. Its manifest requires `openshell.supervisor-middleware.http-session`, so older gateways and supervisors reject it. It does not bind response hooks. + +## Resource bounds and cancellation + +Input and replacement packs stay in unlinked temporary files. The service parses only a bounded 1 MiB receive-pack prefix in memory, feeds the input pack directly to `git index-pack`, and writes the replacement pack directly to its output file. It removes stale integrity headers and declares the exact replacement length. OpenShell then generates Content-Length and receives the replacement in bounded chunks. + +`--max-concurrent-signings` limits requests receiving input or running Git rewrite workers. This bounds concurrent input files as well as signing work. `--signing-timeout-seconds` covers upstream fetches, graph rewriting, signing, and pack generation. If the OpenShell request is canceled or the deadline expires, the service kills the active `git` or `ssh-keygen` subprocess and waits for it to exit. Subprocess diagnostic capture is bounded and is not returned to the sandbox. + +## Prototype limits + +- HTTP session hooks always fail closed. The service denies a selected push when OpenShell does not offer STREAM, including encoded requests. It rejects request trailers. +- The implementation supports SHA-1 repositories and direct `refs/heads/*` updates. It rejects SHA-256 repositories, annotated-tag pushes, push certificates, and other ref types. +- Each push shallow-fetches upstream objects before signing. The host must be able to reach the repository, and private repositories need a non-interactive Git credential helper available to the local middleware user. A production deployment should use a bounded object cache and explicit credential plumbing. +- The service shells out to the host's `git` and `ssh-keygen`. Run it with OS-level CPU, memory, process, temporary-storage, and network limits in addition to its own concurrency and time bounds. +- Rewriting commit object IDs means the sandbox's local branch still points to the unsigned commit after a successful push. A subsequent fetch updates the remote-tracking ref, but the local branch must be reconciled with the rewritten history. This is the largest workflow issue for transparent push-time signing. +- The example has protocol and local end-to-end Git coverage, not a live GitHub push. Test against a disposable repository before using a real signing key. + +These constraints make the approach viable as a focused deployment or proof of concept, but not yet transparent enough for general production pushes. A first-class commit-signing operation invoked before Git creates the final local object would avoid the local/remote object-ID split while still keeping the private key outside the sandbox. diff --git a/examples/supervisor-middleware-git-signing/policy.yaml b/examples/supervisor-middleware-git-signing/policy.yaml new file mode 100644 index 0000000000..45c861bfc2 --- /dev/null +++ b/examples/supervisor-middleware-git-signing/policy.yaml @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /etc] + read_write: [/sandbox, /tmp, /dev/null] + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_middlewares: + sign-git-commits: + name: Sign outgoing Git commits + middleware: local-git-signer + order: 10 + config: {} + on_error: fail_closed + endpoints: + include: [github.com] + +network_policies: + github_git: + name: github-git + endpoints: + - host: github.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: "//.git/info/refs*" + - allow: + method: POST + path: "//.git/git-upload-pack" + - allow: + method: POST + path: "//.git/git-receive-pack" + binaries: + - { path: /usr/bin/git } + - { path: /usr/lib/git-core/git-remote-http } diff --git a/examples/supervisor-middleware-git-signing/src/main.rs b/examples/supervisor-middleware-git-signing/src/main.rs new file mode 100644 index 0000000000..ee7d26e4cc --- /dev/null +++ b/examples/supervisor-middleware-git-signing/src/main.rs @@ -0,0 +1,1131 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +mod signer; + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use clap::Parser; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header}; +use openshell_core::extension_protocol::{ + ExtensionFamily, http_session_middleware_metadata, validate_gateway_metadata, +}; +use openshell_core::middleware::{ + HttpRequestSessionResultStream, HttpResponseSessionResultStream, WebSocketResponseStream, +}; +use openshell_core::proto::middleware::v1::http_session_middleware_server::{ + HttpSessionMiddleware, HttpSessionMiddlewareServer, +}; +use openshell_core::proto::middleware::v1::supervisor_middleware_server::{ + SupervisorMiddleware, SupervisorMiddlewareServer, +}; +use openshell_core::proto::{ + Finding, HeaderMutation, HttpBodyMode, HttpContinue, HttpFinish, HttpInspect, HttpOutputChunk, + HttpOutputStart, HttpPreflightResult, HttpRequestEvaluation, HttpRequestPreflightHead, + HttpRequestResult, HttpRequestSessionEvent, HttpRequestSessionResult, HttpResponseSessionEvent, + HttpStreamMode, MiddlewareBinding, MiddlewareDescribeRequest, MiddlewareDiagnostics, + MiddlewareManifest, RemoveHeader, SupervisorMiddlewareOperation, SupervisorMiddlewarePhase, + ValidateConfigRequest, ValidateConfigResponse, WebSocketSessionEvent, header_mutation, + http_inspect, http_preflight_result, http_request_session_event, http_request_session_result, +}; +use openshell_extension_core::{ + EXTENSION_JWT_TYP, ExtensionCallerKind, ExtensionJwtClaims, MAX_EXTENSION_TOKEN_TTL, +}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio_stream::StreamExt as _; +use tokio_stream::wrappers::ReceiverStream; +use tonic::transport::{Identity, Server, ServerTlsConfig}; +use tonic::{Request, Response, Status}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +use crate::signer::{GitSigner, SignControl}; + +const MANIFEST_NAME: &str = "example/git-commit-signing"; +const OPERATION: SupervisorMiddlewareOperation = SupervisorMiddlewareOperation::HttpRequest; +const PHASE: SupervisorMiddlewarePhase = SupervisorMiddlewarePhase::PreCredentials; +const MAX_UNIT_BYTES: usize = 64 * 1024; +const JWT_CLOCK_SKEW_SECONDS: i64 = 60; + +#[derive(Debug, Parser)] +#[command(about = "Sign commits in Git smart-HTTP pushes outside an OpenShell sandbox")] +struct Cli { + /// Address on which to serve authenticated TLS gRPC. + #[arg(long, default_value = "127.0.0.1:50051")] + bind: SocketAddr, + + /// Local SSH private key used by ssh-keygen. This path is never sent to the sandbox. + #[arg(long)] + signing_key: PathBuf, + + /// PEM certificate presented by this middleware service. + #[arg(long)] + tls_cert: PathBuf, + + /// PEM private key for the middleware TLS certificate. + #[arg(long)] + tls_key: PathBuf, + + /// PEM Ed25519 public key used to verify OpenShell extension JWTs. + #[arg(long)] + extension_public_key: PathBuf, + + /// Gateway ID expected in extension-token issuer claims. + #[arg(long)] + expected_gateway_id: String, + + /// Exact extension JWT audience configured for this registration. + #[arg(long)] + audience: String, + + /// Maximum number of concurrent Git rewrite workers. + #[arg(long, default_value_t = 2)] + max_concurrent_signings: usize, + + /// Total deadline for fetch, rewrite, signing, and pack generation. + #[arg(long, default_value_t = 25)] + signing_timeout_seconds: u64, +} + +struct ExtensionAuth { + decoding_key: DecodingKey, + issuer: String, + audience: String, +} + +impl ExtensionAuth { + fn new(public_key_pem: &[u8], gateway_id: &str, audience: String) -> Result { + if gateway_id.is_empty() || audience.is_empty() { + return Err("expected gateway ID and audience must be nonempty".into()); + } + let decoding_key = DecodingKey::from_ed_pem(public_key_pem) + .map_err(|error| format!("invalid extension public key: {error}"))?; + Ok(Self { + decoding_key, + issuer: format!("openshell-gateway:{gateway_id}"), + audience, + }) + } + + fn authenticate( + &self, + request: &Request, + required_caller: Option, + ) -> Result<(), Status> { + let authorization = request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")) + .ok_or_else(|| Status::unauthenticated("extension bearer token is required"))?; + let header = decode_header(authorization) + .map_err(|_| Status::unauthenticated("extension bearer token is invalid"))?; + if header.alg != Algorithm::EdDSA + || header.typ.as_deref() != Some(EXTENSION_JWT_TYP) + || header.kid.as_deref().is_none_or(str::is_empty) + { + return Err(Status::unauthenticated( + "extension bearer token header is invalid", + )); + } + let mut validation = Validation::new(Algorithm::EdDSA); + validation.set_issuer(&[self.issuer.as_str()]); + validation.set_audience(&[self.audience.as_str()]); + validation.set_required_spec_claims(&["iss", "aud", "sub", "iat", "exp", "jti"]); + let claims = decode::(authorization, &self.decoding_key, &validation) + .map_err(|_| Status::unauthenticated("extension bearer token is invalid"))? + .claims; + if claims.jti.is_empty() + || claims.iat < 0 + || claims.exp <= claims.iat + || u64::try_from(claims.exp - claims.iat) + .ok() + .is_none_or(|ttl| ttl > MAX_EXTENSION_TOKEN_TTL.as_secs()) + { + return Err(Status::unauthenticated( + "extension bearer token lifetime is invalid", + )); + } + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| Status::internal("system clock is before the Unix epoch"))? + .as_secs() as i64; + if claims.iat > now.saturating_add(JWT_CLOCK_SKEW_SECONDS) { + return Err(Status::unauthenticated( + "extension bearer token issue time is invalid", + )); + } + if required_caller.is_some_and(|required| claims.caller_kind != required) { + return Err(Status::permission_denied( + "extension caller kind is not authorized for this RPC", + )); + } + match claims.caller_kind { + ExtensionCallerKind::Gateway + if claims.sandbox_id.is_none() && claims.sub == self.issuer => {} + ExtensionCallerKind::Supervisor => { + let sandbox_id = claims.sandbox_id.as_deref().filter(|id| !id.is_empty()); + if sandbox_id + .is_none_or(|id| claims.sub != format!("spiffe://openshell/sandbox/{id}")) + { + return Err(Status::permission_denied( + "extension supervisor identity is invalid", + )); + } + } + _ => { + return Err(Status::permission_denied( + "extension gateway identity is invalid", + )); + } + } + Ok(()) + } +} + +#[derive(Clone)] +struct GitSigningMiddleware { + signer: Arc, + auth: Arc, + signing_slots: Arc, + signing_timeout: Duration, + expected_audience: String, +} + +impl GitSigningMiddleware { + fn new( + signing_key: PathBuf, + max_concurrent_signings: usize, + signing_timeout: Duration, + auth: ExtensionAuth, + ) -> Result { + if max_concurrent_signings == 0 { + return Err("max concurrent signings must be positive".into()); + } + Ok(Self { + signer: Arc::new(GitSigner::new(signing_key)?), + expected_audience: auth.audience.clone(), + auth: Arc::new(auth), + signing_slots: Arc::new(tokio::sync::Semaphore::new(max_concurrent_signings)), + signing_timeout, + }) + } + + #[cfg(test)] + fn new_for_test(signing_key: PathBuf) -> Result { + Ok(Self { + signer: Arc::new(GitSigner::new(signing_key)?), + auth: Arc::new(ExtensionAuth { + decoding_key: DecodingKey::from_secret(b"unused-test-key"), + issuer: "openshell-gateway:test".into(), + audience: "urn:openshell:extension:test:git-signing".into(), + }), + signing_slots: Arc::new(tokio::sync::Semaphore::new(1)), + signing_timeout: Duration::from_secs(60), + expected_audience: "urn:openshell:extension:test:git-signing".into(), + }) + } + + fn request_stream(&self, events: S) -> HttpRequestSessionResultStream + where + S: tokio_stream::Stream> + + Send + + Unpin + + 'static, + { + let middleware = self.clone(); + let (sender, receiver) = tokio::sync::mpsc::channel(4); + tokio::spawn(async move { + let result = middleware.sign_stream(events, &sender).await; + if let Err(error) = result { + let _ = sender.send(Err(error)).await; + } + }); + Box::pin(ReceiverStream::new(receiver)) + } + + async fn sign_stream(&self, mut events: S, sender: &ResultSender) -> Result<(), Status> + where + S: tokio_stream::Stream> + Unpin, + { + use http_request_session_event::Event; + let preflight = match next_event(&mut events, sender).await? { + Event::Preflight(preflight) => preflight, + _ => return Err(Status::failed_precondition("expected request preflight")), + }; + let Some(request) = preflight.request.as_ref() else { + return Err(Status::invalid_argument("request head is required")); + }; + if !is_receive_pack_request(request) { + send( + sender, + http_request_session_result::Result::PreflightResult(HttpPreflightResult { + decision: Some(http_preflight_result::Decision::ContinueWithoutBody( + HttpContinue {}, + )), + ..Default::default() + }), + ) + .await?; + return Ok(()); + } + let target = request + .target + .as_ref() + .expect("selected request has a target"); + let upstream_url = github_upstream_url(target)?; + let limits = preflight + .limits + .as_ref() + .ok_or_else(|| Status::invalid_argument("body limits are required"))?; + if preflight.phase != PHASE as i32 + || !preflight + .permitted_body_modes + .contains(&(HttpBodyMode::Stream as i32)) + || limits.max_chunk_bytes == 0 + { + return Err(Status::failed_precondition( + "Git signing requires request STREAM mode", + )); + } + let unit_bytes = limits.max_chunk_bytes.min(MAX_UNIT_BYTES as u64) as usize; + let body_limit = limits + .max_output_body_bytes + .unwrap_or(MAX_PUSH_BYTES) + .min(MAX_PUSH_BYTES); + if preflight + .declared_body_bytes + .is_some_and(|bytes| bytes > body_limit) + { + return Err(Status::resource_exhausted( + "Git signing request exceeds storage limit", + )); + } + // Hold the worker permit while receiving input to bound temporary storage. + let permit = tokio::select! { + permit = Arc::clone(&self.signing_slots).acquire_owned() => permit.map_err(|_| Status::unavailable("Git signer is shutting down"))?, + () = sender.closed() => return Err(Status::cancelled("Git signing request was cancelled")), + }; + let file = tempfile::tempfile().map_err(|_| { + Status::resource_exhausted("Git signing temporary storage is unavailable") + })?; + let mut input = tokio::fs::File::from_std(file); + send( + sender, + http_request_session_result::Result::PreflightResult(HttpPreflightResult { + decision: Some(http_preflight_result::Decision::Inspect(HttpInspect { + mode: Some(http_inspect::Mode::Stream(HttpStreamMode {})), + })), + ..Default::default() + }), + ) + .await?; + if !matches!(next_event(&mut events, sender).await?, Event::Begin(_)) { + return Err(Status::failed_precondition( + "expected begin before request body", + )); + } + let mut input_bytes = 0u64; + loop { + match next_event(&mut events, sender).await? { + Event::InputChunk(chunk) => { + input_bytes = input_bytes.saturating_add(chunk.data.len() as u64); + if chunk.data.is_empty() + || chunk.data.len() as u64 > limits.max_chunk_bytes + || input_bytes > body_limit + { + return Err(Status::resource_exhausted( + "Git signing request exceeds storage or chunk limit", + )); + } + input.write_all(&chunk.data).await.map_err(|_| { + Status::resource_exhausted("Git signing storage write failed") + })?; + } + Event::InputEnd(end) => { + if !end.trailers.is_empty() { + return Err(Status::invalid_argument( + "Git signing does not support request trailers", + )); + } + break; + } + _ => { + return Err(Status::failed_precondition( + "expected body input or input_end", + )); + } + } + } + finish_signing( + Arc::clone(&self.signer), + SigningRequest { + input, + upstream_url, + request_id: preflight + .context + .map(|context| context.request_id) + .unwrap_or_default(), + limits: OutputLimits { + body_bytes: body_limit, + unit_bytes, + }, + }, + permit, + self.signing_timeout, + sender, + ) + .await + } +} + +const MAX_PUSH_BYTES: u64 = 256 * 1024 * 1024; +type ResultSender = tokio::sync::mpsc::Sender>; + +async fn next_event( + events: &mut S, + sender: &ResultSender, +) -> Result +where + S: tokio_stream::Stream> + Unpin, +{ + tokio::select! { + event = events.next() => event.ok_or_else(|| Status::cancelled("request input closed"))??.event.ok_or_else(|| Status::invalid_argument("request event is required")), + () = sender.closed() => Err(Status::cancelled("Git signing request was cancelled")), + } +} + +async fn send( + sender: &ResultSender, + result: http_request_session_result::Result, +) -> Result<(), Status> { + sender + .send(Ok(HttpRequestSessionResult { + result: Some(result), + })) + .await + .map_err(|_| Status::cancelled("Git signing request was cancelled")) +} + +#[derive(Clone, Copy)] +struct OutputLimits { + body_bytes: u64, + unit_bytes: usize, +} + +struct SigningRequest { + input: tokio::fs::File, + upstream_url: String, + request_id: String, + limits: OutputLimits, +} + +#[tonic::async_trait] +impl SupervisorMiddleware for GitSigningMiddleware { + type EvaluateWebSocketSessionStream = WebSocketResponseStream; + + async fn describe( + &self, + request: Request, + ) -> Result, Status> { + self.auth.authenticate(&request, None)?; + let manifest = MiddlewareManifest { + name: MANIFEST_NAME.into(), + service_version: env!("CARGO_PKG_VERSION").into(), + bindings: vec![MiddlewareBinding { + operation: OPERATION as i32, + phase: PHASE as i32, + max_payload_bytes: MAX_UNIT_BYTES as u64, + request_timeout: Some(prost_types::Duration { + seconds: 30, + nanos: 0, + }), + }], + expected_audience: self.expected_audience.clone(), + extension: Some(http_session_middleware_metadata( + MANIFEST_NAME, + openshell_core::VERSION, + )), + }; + validate_gateway_metadata( + ExtensionFamily::SupervisorMiddleware, + MANIFEST_NAME, + manifest.extension.as_ref(), + request.into_inner().gateway, + ) + .map_err(|error| Status::failed_precondition(error.to_string()))?; + Ok(Response::new(manifest)) + } + + async fn validate_config( + &self, + request: Request, + ) -> Result, Status> { + self.auth + .authenticate(&request, Some(ExtensionCallerKind::Gateway))?; + let request = request.into_inner(); + let unknown = request + .config + .as_ref() + .and_then(|config| config.fields.keys().next()) + .cloned(); + Ok(Response::new(match unknown { + None => ValidateConfigResponse { + valid: true, + reason: String::new(), + }, + Some(field) => ValidateConfigResponse { + valid: false, + reason: format!("unsupported config field '{field}'"), + }, + })) + } + + async fn evaluate_http_request( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Git signing implements HTTP session hooks only", + )) + } + + async fn evaluate_web_socket_session( + &self, + request: Request>, + ) -> Result, Status> { + self.auth + .authenticate(&request, Some(ExtensionCallerKind::Supervisor))?; + Err(Status::unimplemented( + "WebSocket middleware is not supported", + )) + } +} + +#[tonic::async_trait] +impl HttpSessionMiddleware for GitSigningMiddleware { + type EvaluateRequestStream = HttpRequestSessionResultStream; + type EvaluateResponseStream = HttpResponseSessionResultStream; + + async fn evaluate_request( + &self, + request: Request>, + ) -> Result, Status> { + self.auth + .authenticate(&request, Some(ExtensionCallerKind::Supervisor))?; + Ok(Response::new(self.request_stream(request.into_inner()))) + } + + async fn evaluate_response( + &self, + _request: Request>, + ) -> Result, Status> { + Err(Status::unimplemented( + "response middleware is not supported", + )) + } +} + +async fn finish_signing( + signer: Arc, + request: SigningRequest, + _permit: tokio::sync::OwnedSemaphorePermit, + signing_timeout: Duration, + sender: &ResultSender, +) -> Result<(), Status> { + let SigningRequest { + mut input, + upstream_url, + request_id, + limits, + } = request; + input + .flush() + .await + .map_err(|_| Status::internal("Git signing temporary storage flush failed"))?; + let input = input.into_std().await; + let log_upstream = upstream_url.clone(); + let log_request_id = request_id.clone(); + let cancelled = Arc::new(AtomicBool::new(false)); + let control = SignControl::new(Arc::clone(&cancelled), Instant::now() + signing_timeout); + let mut worker = tokio::task::spawn_blocking(move || { + signer.sign_receive_pack(input, Some(&upstream_url), &control) + }); + let signed = tokio::select! { + result = &mut worker => { + result.map_err(|_| Status::internal("Git signing worker failed"))? + } + () = sender.closed() => { + cancelled.store(true, Ordering::Release); + let _ = worker.await; + return Err(Status::cancelled("Git signing request was cancelled")); + } + () = tokio::time::sleep(signing_timeout) => { + cancelled.store(true, Ordering::Release); + let _ = worker.await; + return Err(Status::deadline_exceeded("Git signing deadline exceeded")); + } + } + .map_err(|error| { + let status = if error.is_cancelled() { + Status::cancelled("Git signing request was cancelled") + } else if error.is_timed_out() { + Status::deadline_exceeded("Git signing deadline exceeded") + } else { + Status::failed_precondition(error.public_message()) + }; + warn!( + request_id = log_request_id, + upstream = %log_upstream, + category = if error.is_cancelled() { "cancelled" } else if error.is_timed_out() { "timeout" } else { "invalid_push" }, + "outgoing Git push could not be signed" + ); + status + })?; + + if signed.body_len > limits.body_bytes { + return Err(Status::resource_exhausted( + "signed Git request exceeds deferred storage limit", + )); + } + + let signed_commits = signed.signed_commits; + let header_mutations = [ + "etag", + "content-md5", + "digest", + "content-digest", + "repr-digest", + "signature", + "signature-input", + ] + .into_iter() + .map(|name| HeaderMutation { + operation: Some(header_mutation::Operation::Remove(RemoveHeader { + name: name.into(), + })), + }) + .collect(); + send( + sender, + http_request_session_result::Result::OutputStart(HttpOutputStart { + output_body_bytes: Some(signed.body_len), + header_mutations, + }), + ) + .await?; + let mut body = tokio::fs::File::from_std(signed.body); + let mut chunk = vec![0; limits.unit_bytes]; + loop { + let read = body + .read(&mut chunk) + .await + .map_err(|_| Status::internal("signed Git storage read failed"))?; + if read == 0 { + break; + } + send( + sender, + http_request_session_result::Result::OutputChunk(HttpOutputChunk { + data: chunk[..read].to_vec(), + }), + ) + .await?; + } + send( + sender, + http_request_session_result::Result::Finish(HttpFinish { + diagnostics: Some(MiddlewareDiagnostics { + reason_code: "git_commits_signed".into(), + findings: vec![Finding { + r#type: "git.commits_signed".into(), + label: "Git commits signed".into(), + count: signed_commits, + confidence: "high".into(), + severity: "informational".into(), + }], + metadata: HashMap::from([( + "signed_commit_count".into(), + signed_commits.to_string(), + )]), + ..Default::default() + }), + ..Default::default() + }), + ) + .await?; + info!( + request_id, + upstream = %log_upstream, + signed_commits, + "signed outgoing Git push" + ); + Ok(()) +} + +fn github_upstream_url( + target: &openshell_core::proto::HttpRequestTarget, +) -> Result { + if target.scheme != "https" || target.host != "github.com" || target.port != 443 { + return Err(Status::invalid_argument( + "prototype supports HTTPS pushes to github.com only", + )); + } + let repository_path = target + .path + .strip_suffix("/git-receive-pack") + .ok_or_else(|| Status::invalid_argument("invalid Git receive-pack path"))?; + let segments = repository_path + .strip_prefix('/') + .and_then(|path| path.strip_suffix(".git")) + .map(|path| path.split('/').collect::>()) + .ok_or_else(|| Status::invalid_argument("invalid GitHub repository path"))?; + if segments.len() != 2 + || segments.iter().any(|segment| { + matches!(*segment, "" | "." | "..") + || !segment + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + }) + { + return Err(Status::invalid_argument("invalid GitHub repository path")); + } + Ok(format!("https://github.com{repository_path}")) +} + +fn is_receive_pack_request(request: &HttpRequestPreflightHead) -> bool { + let Some(target) = request.target.as_ref() else { + return false; + }; + target.method == "POST" + && target.path.ends_with("/git-receive-pack") + && request.headers.iter().any(|header| { + header.name.eq_ignore_ascii_case("content-type") + && header + .value + .split(';') + .next() + .is_some_and(|value| value.trim() == "application/x-git-receive-pack-request") + }) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + let cli = Cli::parse(); + if cli.signing_timeout_seconds == 0 { + return Err("signing timeout must be positive".into()); + } + let tls_cert = std::fs::read(&cli.tls_cert)?; + let tls_key = std::fs::read(&cli.tls_key)?; + let extension_public_key = std::fs::read(&cli.extension_public_key)?; + let auth = ExtensionAuth::new( + &extension_public_key, + &cli.expected_gateway_id, + cli.audience, + ) + .map_err(|error| format!("invalid extension authentication configuration: {error}"))?; + let middleware = GitSigningMiddleware::new( + cli.signing_key, + cli.max_concurrent_signings, + Duration::from_secs(cli.signing_timeout_seconds), + auth, + ) + .map_err(|error| format!("invalid signing configuration: {error}"))?; + info!(bind = %cli.bind, "starting Git commit signing middleware"); + Server::builder() + .tls_config(ServerTlsConfig::new().identity(Identity::from_pem(tls_cert, tls_key)))? + .add_service(SupervisorMiddlewareServer::new(middleware.clone())) + .add_service(HttpSessionMiddlewareServer::new(middleware)) + .serve(cli.bind) + .await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use jsonwebtoken::{EncodingKey, Header, encode}; + use openshell_core::proto::HttpRequestSessionPreflight; + + const TEST_PRIVATE_KEY: &[u8] = br#"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAR9CeOXmiSU6YscHZWTYbW7DUc5uhdO3/OeXZg3j1+u +-----END PRIVATE KEY----- +"#; + const TEST_PUBLIC_KEY: &[u8] = br#"-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAuEbM0q6xP8hFxwY5kd/fD/mwr3ZpA/T7zhx6TN0DKMM= +-----END PUBLIC KEY----- +"#; + + fn authenticated_request(claims: ExtensionJwtClaims) -> Request<()> { + let mut header = Header::new(Algorithm::EdDSA); + header.typ = Some(EXTENSION_JWT_TYP.into()); + header.kid = Some("test-key".into()); + let token = encode( + &header, + &claims, + &EncodingKey::from_ed_pem(TEST_PRIVATE_KEY).unwrap(), + ) + .unwrap(); + let mut request = Request::new(()); + request + .metadata_mut() + .insert("authorization", format!("Bearer {token}").parse().unwrap()); + request + } + + fn test_claims(caller_kind: ExtensionCallerKind) -> ExtensionJwtClaims { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let (sub, sandbox_id) = match caller_kind { + ExtensionCallerKind::Gateway => ("openshell-gateway:test".into(), None), + ExtensionCallerKind::Supervisor => ( + "spiffe://openshell/sandbox/sandbox-test".into(), + Some("sandbox-test".into()), + ), + }; + ExtensionJwtClaims { + iss: "openshell-gateway:test".into(), + aud: "urn:openshell:extension:test:git-signing".into(), + sub, + iat: now, + exp: now + 300, + jti: "unique-test-token".into(), + caller_kind, + sandbox_id, + } + } + + #[test] + fn extension_auth_enforces_rpc_caller_kind() { + let auth = ExtensionAuth::new( + TEST_PUBLIC_KEY, + "test", + "urn:openshell:extension:test:git-signing".into(), + ) + .unwrap(); + + assert!( + auth.authenticate( + &authenticated_request(test_claims(ExtensionCallerKind::Gateway)), + Some(ExtensionCallerKind::Gateway), + ) + .is_ok() + ); + assert_eq!( + auth.authenticate( + &authenticated_request(test_claims(ExtensionCallerKind::Gateway)), + Some(ExtensionCallerKind::Supervisor), + ) + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); + assert!( + auth.authenticate( + &authenticated_request(test_claims(ExtensionCallerKind::Supervisor)), + Some(ExtensionCallerKind::Supervisor), + ) + .is_ok() + ); + } + + #[test] + fn extension_auth_rejects_mismatched_supervisor_identity() { + let auth = ExtensionAuth::new( + TEST_PUBLIC_KEY, + "test", + "urn:openshell:extension:test:git-signing".into(), + ) + .unwrap(); + let mut claims = test_claims(ExtensionCallerKind::Supervisor); + claims.sub = "spiffe://openshell/sandbox/another-sandbox".into(); + + assert_eq!( + auth.authenticate( + &authenticated_request(claims), + Some(ExtensionCallerKind::Supervisor), + ) + .unwrap_err() + .code(), + tonic::Code::PermissionDenied + ); + } + + pub(super) fn receive_pack_preflight() -> HttpRequestSessionPreflight { + HttpRequestSessionPreflight { + phase: PHASE as i32, + request: Some(HttpRequestPreflightHead { + target: Some(openshell_core::proto::HttpRequestTarget { + scheme: "https".into(), + host: "github.com".into(), + port: 443, + method: "POST".into(), + path: "/NVIDIA/OpenShell.git/git-receive-pack".into(), + ..Default::default() + }), + headers: vec![openshell_core::proto::HttpHeader { + name: "content-type".into(), + value: "application/x-git-receive-pack-request".into(), + }], + }), + permitted_body_modes: vec![HttpBodyMode::Stream as i32], + limits: Some(openshell_core::proto::HttpBodyLimits { + max_chunk_bytes: MAX_UNIT_BYTES as u64, + ..Default::default() + }), + ..Default::default() + } + } + + #[test] + fn recognizes_git_receive_pack_only() { + let mut preflight = receive_pack_preflight(); + let request = preflight.request.as_mut().unwrap(); + assert!(is_receive_pack_request(request)); + request.target.as_mut().unwrap().path = "/NVIDIA/OpenShell.git/git-upload-pack".into(); + assert!(!is_receive_pack_request(request)); + } + + #[test] + fn derives_a_bounded_github_upstream_url() { + let mut target = receive_pack_preflight().request.unwrap().target.unwrap(); + assert_eq!( + github_upstream_url(&target).unwrap(), + "https://github.com/NVIDIA/OpenShell.git" + ); + target.path = "/../repo.git/git-receive-pack".into(); + assert!(github_upstream_url(&target).is_err()); + } + async fn preflight_result( + preflight: HttpRequestSessionPreflight, + ) -> Result { + let key = tempfile::NamedTempFile::new().unwrap(); + let middleware = GitSigningMiddleware::new_for_test(key.path().to_path_buf()).unwrap(); + middleware + .request_stream(tokio_stream::iter([Ok(HttpRequestSessionEvent { + event: Some(http_request_session_event::Event::Preflight(preflight)), + })])) + .next() + .await + .unwrap() + } + + #[tokio::test] + async fn skips_fetches_but_refuses_uninspectable_pushes() { + let mut fetch = receive_pack_preflight(); + fetch + .request + .as_mut() + .unwrap() + .target + .as_mut() + .unwrap() + .path = "/NVIDIA/OpenShell.git/git-upload-pack".into(); + let result = preflight_result(fetch).await.unwrap(); + assert!(matches!( + result.result, + Some(http_request_session_result::Result::PreflightResult( + HttpPreflightResult { + decision: Some(http_preflight_result::Decision::ContinueWithoutBody(_)), + .. + } + )) + )); + let mut encoded_push = receive_pack_preflight(); + encoded_push.permitted_body_modes.clear(); + assert_eq!( + preflight_result(encoded_push).await.unwrap_err().code(), + tonic::Code::FailedPrecondition + ); + } + + #[tokio::test] + async fn rejects_declared_pushes_above_route_limit() { + let mut preflight = receive_pack_preflight(); + preflight.declared_body_bytes = Some(1025); + preflight.limits.as_mut().unwrap().max_output_body_bytes = Some(1024); + assert_eq!( + preflight_result(preflight).await.unwrap_err().code(), + tonic::Code::ResourceExhausted + ); + } + + #[tokio::test] + async fn withholds_output_until_the_complete_push_arrives() { + use http_request_session_event::Event; + let key = tempfile::NamedTempFile::new().unwrap(); + let middleware = GitSigningMiddleware::new_for_test(key.path().to_path_buf()).unwrap(); + let (sender, receiver) = tokio::sync::mpsc::channel(2); + let mut results = middleware.request_stream(ReceiverStream::new(receiver)); + sender + .send(Ok(HttpRequestSessionEvent { + event: Some(Event::Preflight(receive_pack_preflight())), + })) + .await + .unwrap(); + results.next().await.unwrap().unwrap(); + sender + .send(Ok(HttpRequestSessionEvent { + event: Some(Event::Begin(openshell_core::proto::HttpBegin::default())), + })) + .await + .unwrap(); + // More than the old unary cap, with no complete pack and no input_end. + for _ in 0..65 { + sender + .send(Ok(HttpRequestSessionEvent { + event: Some(Event::InputChunk(openshell_core::proto::HttpInputChunk { + data: vec![b'x'; MAX_UNIT_BYTES], + })), + })) + .await + .unwrap(); + } + assert!( + tokio::time::timeout(Duration::from_millis(50), results.next()) + .await + .is_err() + ); + drop(sender); + assert_eq!( + results.next().await.unwrap().unwrap_err().code(), + tonic::Code::Cancelled + ); + assert!(results.next().await.is_none()); + } + + #[tokio::test] + async fn rejects_trailers_and_missing_begin_without_output() { + use http_request_session_event::Event; + for (begin, trailers) in [ + (false, Vec::new()), + ( + true, + vec![openshell_core::proto::HttpHeader { + name: "digest".into(), + value: "stale".into(), + }], + ), + ] { + let key = tempfile::NamedTempFile::new().unwrap(); + let middleware = GitSigningMiddleware::new_for_test(key.path().to_path_buf()).unwrap(); + let mut events = vec![Event::Preflight(receive_pack_preflight())]; + if begin { + events.push(Event::Begin(openshell_core::proto::HttpBegin::default())); + } + events.push(Event::InputEnd(openshell_core::proto::HttpInputEnd { + trailers, + })); + let mut results = middleware.request_stream(tokio_stream::iter( + events + .into_iter() + .map(|event| Ok(HttpRequestSessionEvent { event: Some(event) })), + )); + results.next().await.unwrap().unwrap(); + let error = results.next().await.unwrap().unwrap_err(); + assert_eq!( + error.code(), + if begin { + tonic::Code::InvalidArgument + } else { + tonic::Code::FailedPrecondition + } + ); + assert!(results.next().await.is_none()); + } + } + #[tokio::test] + async fn serves_session_hooks_over_authenticated_tls() { + use openshell_core::proto::middleware::v1::http_session_middleware_client::HttpSessionMiddlewareClient; + use tokio_stream::wrappers::TcpListenerStream; + use tonic::transport::{Certificate, ClientTlsConfig, Endpoint}; + + let certificate = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_pem = certificate.cert.pem(); + let key = tempfile::NamedTempFile::new().unwrap(); + let auth = ExtensionAuth::new( + TEST_PUBLIC_KEY, + "test", + "urn:openshell:extension:test:git-signing".into(), + ) + .unwrap(); + let middleware = + GitSigningMiddleware::new(key.path().to_path_buf(), 1, Duration::from_secs(10), auth) + .unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let (stop, stopped) = tokio::sync::oneshot::channel(); + let server = tokio::spawn( + Server::builder() + .tls_config(ServerTlsConfig::new().identity(Identity::from_pem( + cert_pem.clone(), + certificate.signing_key.serialize_pem(), + ))) + .unwrap() + .add_service(SupervisorMiddlewareServer::new(middleware.clone())) + .add_service(HttpSessionMiddlewareServer::new(middleware)) + .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async { + let _ = stopped.await; + }), + ); + let channel = Endpoint::from_shared(format!("https://{address}")) + .unwrap() + .tls_config( + ClientTlsConfig::new() + .domain_name("localhost") + .ca_certificate(Certificate::from_pem(cert_pem)), + ) + .unwrap() + .connect() + .await + .unwrap(); + let mut client = HttpSessionMiddlewareClient::new(channel); + let event = || { + let mut preflight = receive_pack_preflight(); + preflight + .request + .as_mut() + .unwrap() + .target + .as_mut() + .unwrap() + .method = "GET".into(); + HttpRequestSessionEvent { + event: Some(http_request_session_event::Event::Preflight(preflight)), + } + }; + assert_eq!( + client + .evaluate_request(tokio_stream::iter([event()])) + .await + .unwrap_err() + .code(), + tonic::Code::Unauthenticated + ); + let request = authenticated_request(test_claims(ExtensionCallerKind::Supervisor)) + .map(|_| tokio_stream::iter([event()])); + let mut results = client.evaluate_request(request).await.unwrap().into_inner(); + assert!(matches!( + results.message().await.unwrap().unwrap().result, + Some(http_request_session_result::Result::PreflightResult( + HttpPreflightResult { + decision: Some(http_preflight_result::Decision::ContinueWithoutBody(_)), + .. + } + )) + )); + assert!(results.message().await.unwrap().is_none()); + stop.send(()).unwrap(); + server.await.unwrap().unwrap(); + } +} diff --git a/examples/supervisor-middleware-git-signing/src/signer.rs b/examples/supervisor-middleware-git-signing/src/signer.rs new file mode 100644 index 0000000000..6c1de93967 --- /dev/null +++ b/examples/supervisor-middleware-git-signing/src/signer.rs @@ -0,0 +1,1126 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fs::{self, File}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +use tempfile::TempDir; + +const SHA1_HEX_LEN: usize = 40; +const ZERO_SHA1: &str = "0000000000000000000000000000000000000000"; +const MAX_RECEIVE_PACK_PREFIX_BYTES: usize = 1024 * 1024; +const MAX_CAPTURE_BYTES: usize = 64 * 1024 * 1024; +const CHILD_POLL_INTERVAL: Duration = Duration::from_millis(10); + +pub struct GitSigner { + signing_key: PathBuf, + upstream_override: Option, +} + +pub struct SignedPush { + pub body: File, + pub body_len: u64, + pub signed_commits: u32, +} + +#[derive(Clone)] +pub struct SignControl { + cancelled: Arc, + deadline: Instant, +} + +impl SignControl { + pub fn new(cancelled: Arc, deadline: Instant) -> Self { + Self { + cancelled, + deadline, + } + } + + fn check(&self) -> Result<(), SignError> { + if self.cancelled.load(Ordering::Acquire) { + return Err(SignError::cancelled()); + } + if Instant::now() >= self.deadline { + return Err(SignError::timed_out()); + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct SignError { + message: String, + kind: SignErrorKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SignErrorKind { + InvalidRequest, + Cancelled, + TimedOut, +} + +impl SignError { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + kind: SignErrorKind::InvalidRequest, + } + } + + fn cancelled() -> Self { + Self { + message: "Git signing was cancelled".into(), + kind: SignErrorKind::Cancelled, + } + } + + fn timed_out() -> Self { + Self { + message: "Git signing exceeded its deadline".into(), + kind: SignErrorKind::TimedOut, + } + } + + pub fn is_cancelled(&self) -> bool { + self.kind == SignErrorKind::Cancelled + } + + pub fn is_timed_out(&self) -> bool { + self.kind == SignErrorKind::TimedOut + } + + pub fn public_message(&self) -> &'static str { + "outgoing Git push could not be signed" + } +} + +impl fmt::Display for SignError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for SignError {} + +impl GitSigner { + pub fn new(signing_key: PathBuf) -> Result { + if !signing_key.is_file() { + return Err("the signing key must name a readable file".into()); + } + Ok(Self { + signing_key, + upstream_override: None, + }) + } + + #[cfg(test)] + fn new_with_upstream_override( + signing_key: PathBuf, + upstream_override: String, + ) -> Result { + let mut signer = Self::new(signing_key)?; + signer.upstream_override = Some(upstream_override); + Ok(signer) + } + + pub fn sign_receive_pack( + &self, + mut body: File, + upstream_url: Option<&str>, + control: &SignControl, + ) -> Result { + control.check()?; + let mut parsed = ReceivePackRequest::parse_file(&mut body)?; + if parsed.updates.is_empty() { + return Err(SignError::new( + "receive-pack request contains no ref updates", + )); + } + + let workspace = TempDir::new().map_err(|error| SignError::new(error.to_string()))?; + let repo = workspace.path().join("objects.git"); + run_git_controlled( + None, + &["init", "--bare", repo_string(&repo)?], + None, + control, + )?; + if let Some(upstream_url) = self.upstream_override.as_deref().or(upstream_url) { + hydrate_upstream(&repo, upstream_url, &parsed.updates, control)?; + } + body.seek(SeekFrom::Start(parsed.pack_offset)) + .map_err(|error| SignError::new(error.to_string()))?; + run_git_file_input( + Some(&repo), + &["index-pack", "--stdin", "--fix-thin"], + body, + control, + )?; + + let mut commit_ids = HashSet::new(); + for update in &parsed.updates { + if update.new_oid == ZERO_SHA1 { + continue; + } + control.check()?; + let output = run_git_controlled( + Some(&repo), + &["rev-list", &update.new_oid, "--not", "--all"], + None, + control, + )?; + let output = String::from_utf8(output) + .map_err(|_| SignError::new("git returned a non-UTF-8 commit list"))?; + commit_ids.extend(output.lines().map(str::to_string)); + } + let mut rewriter = CommitRewriter { + repo: &repo, + signing_key: &self.signing_key, + commit_ids, + rewritten: HashMap::new(), + active: HashSet::new(), + signed_count: 0, + workspace: workspace.path(), + control, + }; + + for update in &mut parsed.updates { + if update.new_oid == ZERO_SHA1 { + continue; + } + if !update.ref_name.starts_with("refs/heads/") { + return Err(SignError::new( + "prototype supports direct branch updates only", + )); + } + if !rewriter.commit_ids.contains(&update.new_oid) { + return Err(SignError::new( + "branch tip commit is not self-contained in the push pack", + )); + } + update.new_oid = rewriter.rewrite(&update.new_oid)?; + } + + if rewriter.signed_count == 0 { + return Err(SignError::new( + "receive-pack request contains no commits to sign", + )); + } + + let base_oids = list_ref_oids(&repo, "refs/middleware", control)?; + let mut revisions = parsed + .updates + .iter() + .filter(|update| update.new_oid != ZERO_SHA1) + .map(|update| update.new_oid.clone()) + .collect::>(); + revisions.extend(base_oids.into_iter().map(|oid| format!("^{oid}"))); + let revision_input = revisions.join("\n") + "\n"; + let mut prefix = parsed.prefix; + for update in &parsed.updates { + prefix[update.new_oid_range.clone()].copy_from_slice(update.new_oid.as_bytes()); + } + let mut result = tempfile::tempfile().map_err(|error| SignError::new(error.to_string()))?; + result + .write_all(&prefix) + .map_err(|error| SignError::new(error.to_string()))?; + run_git_to_file( + Some(&repo), + &["pack-objects", "--stdout", "--revs", "--thin"], + Some(revision_input.as_bytes()), + &mut result, + control, + )?; + let body_len = result + .metadata() + .map_err(|error| SignError::new(error.to_string()))? + .len(); + result + .seek(SeekFrom::Start(0)) + .map_err(|error| SignError::new(error.to_string()))?; + Ok(SignedPush { + body: result, + body_len, + signed_commits: rewriter.signed_count, + }) + } +} + +struct ReceivePackRequest { + prefix: Vec, + pack_offset: u64, + updates: Vec, +} + +struct RefUpdate { + old_oid: String, + new_oid: String, + ref_name: String, + new_oid_range: std::ops::Range, +} + +impl ReceivePackRequest { + fn parse_file(body: &mut File) -> Result { + body.seek(SeekFrom::Start(0)) + .map_err(|error| SignError::new(error.to_string()))?; + let mut prefix = Vec::new(); + let mut updates = Vec::new(); + let mut offset = 0; + let mut command_section = true; + let pack_offset = loop { + let mut marker = [0u8; 4]; + body.read_exact(&mut marker) + .map_err(|_| SignError::new("receive-pack request has no packfile"))?; + if !command_section && marker == *b"PACK" { + body.seek(SeekFrom::Current(-4)) + .map_err(|error| SignError::new(error.to_string()))?; + break offset as u64; + } + let length = parse_pkt_length(&marker)?; + prefix.extend_from_slice(&marker); + if prefix.len() > MAX_RECEIVE_PACK_PREFIX_BYTES { + return Err(SignError::new("receive-pack command prefix is too large")); + } + if length == 0 { + offset += 4; + command_section = false; + continue; + } + if length < 4 { + return Err(SignError::new("invalid receive-pack pkt-line length")); + } + let payload_start = offset + 4; + let mut payload = vec![0; length - 4]; + body.read_exact(&mut payload) + .map_err(|_| SignError::new("truncated receive-pack pkt-line"))?; + prefix.extend_from_slice(&payload); + if prefix.len() > MAX_RECEIVE_PACK_PREFIX_BYTES { + return Err(SignError::new("receive-pack command prefix is too large")); + } + if !command_section { + // Push options, when negotiated, are pkt-lines between the + // command flush and the packfile. Preserve them unchanged. + offset += length; + continue; + } + let command = payload.split(|byte| *byte == 0).next().unwrap_or(&payload); + let command = command.strip_suffix(b"\n").unwrap_or(command); + let first_space = command + .iter() + .position(|byte| *byte == b' ') + .ok_or_else(|| SignError::new("invalid receive-pack ref command"))?; + let second_space = command[first_space + 1..] + .iter() + .position(|byte| *byte == b' ') + .map(|index| first_space + 1 + index) + .ok_or_else(|| SignError::new("invalid receive-pack ref command"))?; + if first_space != SHA1_HEX_LEN || second_space - first_space - 1 != SHA1_HEX_LEN { + return Err(SignError::new( + "prototype supports SHA-1 Git repositories only", + )); + } + let new_start = payload_start + first_space + 1; + let old_oid = ascii_oid(&prefix[payload_start..payload_start + SHA1_HEX_LEN])?; + let new_oid = ascii_oid(&prefix[new_start..new_start + SHA1_HEX_LEN])?; + let ref_name = std::str::from_utf8(&command[second_space + 1..]) + .map_err(|_| SignError::new("receive-pack ref name is not UTF-8"))? + .to_string(); + updates.push(RefUpdate { + old_oid, + new_oid, + ref_name, + new_oid_range: new_start..new_start + SHA1_HEX_LEN, + }); + offset += length; + }; + + Ok(Self { + prefix, + pack_offset, + updates, + }) + } +} + +fn hydrate_upstream( + repo: &Path, + upstream_url: &str, + updates: &[RefUpdate], + control: &SignControl, +) -> Result<(), SignError> { + run_git_controlled( + Some(repo), + &[ + "-c", + "credential.interactive=false", + "fetch", + "--no-tags", + "--depth=1", + upstream_url, + "+HEAD:refs/middleware/upstream-head", + ], + None, + control, + )?; + for (index, old_oid) in updates + .iter() + .map(|update| update.old_oid.as_str()) + .filter(|oid| *oid != ZERO_SHA1) + .collect::>() + .into_iter() + .enumerate() + { + let destination = format!("+{old_oid}:refs/middleware/base-{index}"); + run_git_controlled( + Some(repo), + &[ + "-c", + "credential.interactive=false", + "fetch", + "--no-tags", + "--depth=1", + upstream_url, + &destination, + ], + None, + control, + )?; + } + Ok(()) +} + +fn list_ref_oids( + repo: &Path, + prefix: &str, + control: &SignControl, +) -> Result, SignError> { + let output = run_git_controlled( + Some(repo), + &["for-each-ref", "--format=%(objectname)", prefix], + None, + control, + )?; + let output = String::from_utf8(output) + .map_err(|_| SignError::new("git returned a non-UTF-8 ref list"))?; + Ok(output.lines().map(str::to_string).collect()) +} + +fn parse_pkt_length(bytes: &[u8]) -> Result { + let text = + std::str::from_utf8(bytes).map_err(|_| SignError::new("pkt-line length is not ASCII"))?; + usize::from_str_radix(text, 16).map_err(|_| SignError::new("invalid pkt-line length")) +} + +fn ascii_oid(bytes: &[u8]) -> Result { + if bytes.len() != SHA1_HEX_LEN || !bytes.iter().all(u8::is_ascii_hexdigit) { + return Err(SignError::new("invalid SHA-1 object id")); + } + String::from_utf8(bytes.to_vec()).map_err(|_| SignError::new("invalid SHA-1 object id")) +} + +struct CommitRewriter<'a> { + repo: &'a Path, + signing_key: &'a Path, + commit_ids: HashSet, + rewritten: HashMap, + active: HashSet, + signed_count: u32, + workspace: &'a Path, + control: &'a SignControl, +} + +impl CommitRewriter<'_> { + fn rewrite(&mut self, oid: &str) -> Result { + self.control.check()?; + if let Some(rewritten) = self.rewritten.get(oid) { + return Ok(rewritten.clone()); + } + if !self.commit_ids.contains(oid) { + return Ok(oid.to_string()); + } + if !self.active.insert(oid.to_string()) { + return Err(SignError::new("commit graph contains a cycle")); + } + + let raw = run_git_controlled( + Some(self.repo), + &["cat-file", "commit", oid], + None, + self.control, + )?; + let parsed = ParsedCommit::parse(&raw)?; + let mut parents = Vec::with_capacity(parsed.parents.len()); + for parent in &parsed.parents { + parents.push(self.rewrite(parent)?); + } + let unsigned = parsed.unsigned_with_parents(&parents); + let signature = self.sign_payload(oid, &unsigned)?; + let signed = insert_signature(&unsigned, &signature)?; + let new_oid = String::from_utf8(run_git_controlled( + Some(self.repo), + &["hash-object", "-t", "commit", "-w", "--stdin"], + Some(&signed), + self.control, + )?) + .map_err(|_| SignError::new("git returned a non-UTF-8 object id"))? + .trim() + .to_string(); + + self.active.remove(oid); + self.rewritten.insert(oid.to_string(), new_oid.clone()); + self.signed_count = self.signed_count.saturating_add(1); + Ok(new_oid) + } + + fn sign_payload(&self, oid: &str, payload: &[u8]) -> Result, SignError> { + let payload_path = self.workspace.join(format!("commit-{oid}")); + fs::write(&payload_path, payload).map_err(|error| SignError::new(error.to_string()))?; + let mut command = Command::new("ssh-keygen"); + command + .args(["-Y", "sign", "-n", "git", "-f"]) + .arg(self.signing_key) + .arg(&payload_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()); + run_command_controlled(command, None, "ssh-keygen", self.control)?; + // ssh-keygen appends `.sig` to the input path. + let signature_path = PathBuf::from(format!("{}.sig", payload_path.display())); + fs::read(signature_path).map_err(|error| SignError::new(error.to_string())) + } +} + +struct ParsedCommit { + headers: Vec
, + parents: Vec, + message: Vec, +} + +struct Header { + name: Vec, + block: Vec, +} + +impl ParsedCommit { + fn parse(raw: &[u8]) -> Result { + let separator = raw + .windows(2) + .position(|window| window == b"\n\n") + .ok_or_else(|| SignError::new("commit object has no header separator"))?; + let header_bytes = &raw[..separator]; + let mut headers: Vec
= Vec::new(); + for line in header_bytes.split(|byte| *byte == b'\n') { + if line.starts_with(b" ") { + let previous = headers + .last_mut() + .ok_or_else(|| SignError::new("commit starts with a continuation header"))?; + previous.block.push(b'\n'); + previous.block.extend_from_slice(line); + continue; + } + let name_end = line + .iter() + .position(|byte| *byte == b' ') + .ok_or_else(|| SignError::new("invalid commit header"))?; + headers.push(Header { + name: line[..name_end].to_vec(), + block: line.to_vec(), + }); + } + let parents = headers + .iter() + .filter(|header| header.name == b"parent") + .map(|header| ascii_oid(&header.block[b"parent ".len()..])) + .collect::, _>>()?; + if !headers.iter().any(|header| header.name == b"tree") { + return Err(SignError::new("commit object has no tree")); + } + Ok(Self { + headers, + parents, + message: raw[separator + 2..].to_vec(), + }) + } + + fn unsigned_with_parents(&self, parents: &[String]) -> Vec { + let mut result = Vec::new(); + let mut parent_index = 0; + for header in &self.headers { + if header.name == b"gpgsig" || header.name == b"gpgsig-sha256" { + continue; + } + if header.name == b"parent" { + result.extend_from_slice(b"parent "); + result.extend_from_slice(parents[parent_index].as_bytes()); + parent_index += 1; + } else { + result.extend_from_slice(&header.block); + } + result.push(b'\n'); + } + result.push(b'\n'); + result.extend_from_slice(&self.message); + result + } +} + +fn insert_signature(unsigned: &[u8], signature: &[u8]) -> Result, SignError> { + let separator = unsigned + .windows(2) + .position(|window| window == b"\n\n") + .ok_or_else(|| SignError::new("unsigned commit has no header separator"))?; + let signature = signature.strip_suffix(b"\n").unwrap_or(signature); + let mut result = Vec::with_capacity(unsigned.len() + signature.len() + 16); + result.extend_from_slice(&unsigned[..separator + 1]); + for (index, line) in signature.split(|byte| *byte == b'\n').enumerate() { + result.extend_from_slice(if index == 0 { b"gpgsig " } else { b" " }); + result.extend_from_slice(line); + result.push(b'\n'); + } + result.extend_from_slice(&unsigned[separator + 1..]); + Ok(result) +} + +fn repo_string(path: &Path) -> Result<&str, SignError> { + path.to_str() + .ok_or_else(|| SignError::new("temporary repository path is not UTF-8")) +} + +fn run_git_controlled( + repo: Option<&Path>, + args: &[&str], + input: Option<&[u8]>, + control: &SignControl, +) -> Result, SignError> { + let mut command = Command::new("git"); + if let Some(repo) = repo { + command.arg("-C").arg(repo); + } + command + .args(args) + .stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + run_command_controlled( + command, + input, + args.first().copied().unwrap_or("command"), + control, + ) +} + +fn run_git_file_input( + repo: Option<&Path>, + args: &[&str], + input: File, + control: &SignControl, +) -> Result, SignError> { + let mut command = Command::new("git"); + if let Some(repo) = repo { + command.arg("-C").arg(repo); + } + command + .args(args) + .stdin(Stdio::from(input)) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + run_command_controlled( + command, + None, + args.first().copied().unwrap_or("command"), + control, + ) +} + +fn run_git_to_file( + repo: Option<&Path>, + args: &[&str], + input: Option<&[u8]>, + output: &mut File, + control: &SignControl, +) -> Result<(), SignError> { + output + .seek(SeekFrom::End(0)) + .map_err(|error| SignError::new(error.to_string()))?; + let output_handle = output + .try_clone() + .map_err(|error| SignError::new(error.to_string()))?; + let mut command = Command::new("git"); + if let Some(repo) = repo { + command.arg("-C").arg(repo); + } + command + .args(args) + .stdin(if input.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .stdout(Stdio::from(output_handle)) + .stderr(Stdio::piped()); + run_command_controlled( + command, + input, + args.first().copied().unwrap_or("command"), + control, + )?; + Ok(()) +} + +fn run_command_controlled( + mut command: Command, + input: Option<&[u8]>, + command_name: &str, + control: &SignControl, +) -> Result, SignError> { + control.check()?; + let mut child = command + .spawn() + .map_err(|error| SignError::new(format!("could not run {command_name}: {error}")))?; + let stdout = child.stdout.take().map(spawn_pipe_drain); + let stderr = child.stderr.take().map(spawn_pipe_drain); + if let Some(input) = input { + let write_result = child + .stdin + .take() + .ok_or_else(|| SignError::new(format!("{command_name} stdin was unavailable")))? + .write_all(input); + if let Err(error) = write_result { + let _ = child.kill(); + let _ = child.wait(); + return Err(SignError::new(error.to_string())); + } + } + drop(child.stdin.take()); + + let status = loop { + if let Err(error) = control.check() { + let _ = child.kill(); + let _ = child.wait(); + join_pipe(stdout)?; + join_pipe(stderr)?; + return Err(error); + } + match child + .try_wait() + .map_err(|error| SignError::new(error.to_string()))? + { + Some(status) => break status, + None => std::thread::sleep(CHILD_POLL_INTERVAL), + } + }; + let stdout = join_pipe(stdout)?; + let stderr = join_pipe(stderr)?; + if !status.success() { + let stderr = String::from_utf8_lossy(&stderr); + return Err(SignError::new(format!( + "{command_name} failed: {}", + stderr.trim() + ))); + } + Ok(stdout) +} + +type PipeDrain = std::thread::JoinHandle>>; + +fn spawn_pipe_drain(mut pipe: R) -> PipeDrain +where + R: Read + Send + 'static, +{ + std::thread::spawn(move || { + let mut captured = Vec::new(); + let mut buffer = [0u8; 8192]; + loop { + let read = pipe.read(&mut buffer)?; + if read == 0 { + return Ok(captured); + } + if captured.len() < MAX_CAPTURE_BYTES { + let keep = read.min(MAX_CAPTURE_BYTES - captured.len()); + captured.extend_from_slice(&buffer[..keep]); + } + } + }) +} + +fn join_pipe(drain: Option) -> Result, SignError> { + drain.map_or_else( + || Ok(Vec::new()), + |drain| { + drain + .join() + .map_err(|_| SignError::new("subprocess output worker failed"))? + .map_err(|error| SignError::new(error.to_string())) + }, + ) +} + +#[cfg(test)] +fn run_git(repo: Option<&Path>, args: &[&str], input: Option<&[u8]>) -> Result, SignError> { + let control = SignControl::new( + Arc::new(AtomicBool::new(false)), + Instant::now() + Duration::from_secs(300), + ); + run_git_controlled(repo, args, input, &control) +} + +#[cfg(test)] +mod tests { + use super::*; + use openshell_core::proto::{ + HttpBegin, HttpInputChunk, HttpInputEnd, HttpRequestSessionEvent, + http_request_session_event, http_request_session_result, + }; + use tokio_stream::StreamExt as _; + + #[cfg(unix)] + #[test] + fn cancellation_terminates_an_active_subprocess() { + let cancelled = Arc::new(AtomicBool::new(false)); + let control = SignControl::new( + Arc::clone(&cancelled), + Instant::now() + Duration::from_secs(30), + ); + let started = Instant::now(); + let worker = std::thread::spawn(move || { + let mut command = Command::new("sleep"); + command + .arg("30") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + run_command_controlled(command, None, "sleep", &control) + }); + + std::thread::sleep(Duration::from_millis(50)); + cancelled.store(true, Ordering::Release); + let error = worker + .join() + .expect("subprocess worker must not panic") + .expect_err("cancellation must terminate the subprocess"); + + assert!(error.is_cancelled()); + assert!(started.elapsed() < Duration::from_secs(2)); + } + + #[tokio::test] + async fn signs_every_commit_and_rewrites_the_branch_tip() { + let fixture = TempDir::new().unwrap(); + let source = fixture.path().join("source.git"); + run_git( + None, + &["init", "--bare", repo_string(&source).unwrap()], + None, + ) + .unwrap(); + let large_blob = pseudo_random_bytes(5 * 1024 * 1024); + let blob = object(&source, "blob", &large_blob); + let tree_line = format!("100644 blob {blob}\tREADME.md\n"); + let tree = String::from_utf8( + run_git(Some(&source), &["mktree"], Some(tree_line.as_bytes())).unwrap(), + ) + .unwrap() + .trim() + .to_string(); + let first = unsigned_commit(&source, &tree, None, "first"); + let second = unsigned_commit(&source, &tree, Some(&first), "second"); + let upstream_head = unsigned_commit(&source, &tree, None, "upstream"); + run_git( + Some(&source), + &["update-ref", "refs/heads/main", &upstream_head], + None, + ) + .unwrap(); + run_git( + Some(&source), + &["symbolic-ref", "HEAD", "refs/heads/main"], + None, + ) + .unwrap(); + let objects = format!("{blob}\n{tree}\n{first}\n{second}\n"); + let pack = run_git( + Some(&source), + &["pack-objects", "--stdout"], + Some(objects.as_bytes()), + ) + .unwrap(); + let body = receive_pack_body(&second, &pack); + assert!(body.len() > 4 * 1024 * 1024); + + let key = fixture.path().join("signing-key"); + let status = Command::new("ssh-keygen") + .args(["-q", "-t", "ed25519", "-N", "", "-f"]) + .arg(&key) + .status() + .unwrap(); + assert!(status.success()); + let signed = sign_through_session_stream(&key, &source, &body).await; + assert_eq!(signed.signed_commits, 2); + + let mut signed_file = file_with_bytes(&signed.body); + let parsed = ReceivePackRequest::parse_file(&mut signed_file).unwrap(); + let new_tip = parsed.updates[0].new_oid.clone(); + assert_ne!(new_tip, second); + let verify = fixture.path().join("verify.git"); + run_git( + None, + &["init", "--bare", repo_string(&verify).unwrap()], + None, + ) + .unwrap(); + run_git( + None, + &[ + "receive-pack", + "--stateless-rpc", + repo_string(&verify).unwrap(), + ], + Some(&signed.body), + ) + .unwrap(); + let received_tip = String::from_utf8( + run_git(Some(&verify), &["rev-parse", "refs/heads/main"], None).unwrap(), + ) + .unwrap(); + assert_eq!(received_tip.trim(), new_tip); + let tip = run_git(Some(&verify), &["cat-file", "commit", &new_tip], None).unwrap(); + assert!(tip.windows(7).any(|window| window == b"gpgsig ")); + let tip = ParsedCommit::parse(&tip).unwrap(); + assert_eq!(tip.parents.len(), 1); + assert_ne!(tip.parents[0], first); + let parent = run_git( + Some(&verify), + &["cat-file", "commit", &tip.parents[0]], + None, + ) + .unwrap(); + assert!(parent.windows(7).any(|window| window == b"gpgsig ")); + + let allowed = fixture.path().join("allowed-signers"); + let public_key = fs::read_to_string(key.with_extension("pub")).unwrap(); + fs::write(&allowed, format!("agent@example.com {}", public_key.trim())).unwrap(); + verify_commit(&verify, &allowed, &new_tip); + verify_commit(&verify, &allowed, &tip.parents[0]); + } + + struct CollectedSignedPush { + body: Vec, + signed_commits: u32, + } + + async fn sign_through_session_stream( + key: &Path, + upstream: &Path, + body: &[u8], + ) -> CollectedSignedPush { + let signer = GitSigner::new_with_upstream_override( + key.to_path_buf(), + repo_string(upstream).unwrap().to_string(), + ) + .unwrap(); + let mut middleware = crate::GitSigningMiddleware::new_for_test(key.to_path_buf()).unwrap(); + middleware.signer = std::sync::Arc::new(signer); + use http_request_session_event::Event; + use http_request_session_result::Result as Reply; + let mut events = vec![ + Ok(HttpRequestSessionEvent { + event: Some(Event::Preflight(crate::tests::receive_pack_preflight())), + }), + Ok(HttpRequestSessionEvent { + event: Some(Event::Begin(HttpBegin::default())), + }), + ]; + for chunk in body.chunks(crate::MAX_UNIT_BYTES) { + events.push(Ok(HttpRequestSessionEvent { + event: Some(Event::InputChunk(HttpInputChunk { + data: chunk.to_vec(), + })), + })); + } + events.push(Ok(HttpRequestSessionEvent { + event: Some(Event::InputEnd(HttpInputEnd::default())), + })); + let mut results = middleware.request_stream(tokio_stream::iter(events)); + assert!(matches!( + results.next().await.unwrap().unwrap().result, + Some(Reply::PreflightResult(_)) + )); + let Some(Reply::OutputStart(start)) = results.next().await.unwrap().unwrap().result else { + panic!("expected output_start after complete input"); + }; + let mut output = Vec::new(); + let signed_commits = loop { + match results.next().await.unwrap().unwrap().result { + Some(Reply::OutputChunk(chunk)) => { + assert!(!chunk.data.is_empty()); + assert!(chunk.data.len() <= crate::MAX_UNIT_BYTES); + output.extend_from_slice(&chunk.data); + } + Some(Reply::Finish(finish)) => break finish.diagnostics.unwrap().findings[0].count, + other => panic!("unexpected result: {other:?}"), + } + }; + assert_eq!(start.output_body_bytes, Some(output.len() as u64)); + assert_eq!(start.header_mutations.len(), 7); + assert!(results.next().await.is_none()); + CollectedSignedPush { + body: output, + signed_commits, + } + } + + #[test] + fn rejects_non_branch_updates() { + let payload = format!("{ZERO_SHA1} {ZERO_SHA1} refs/tags/v1\n"); + let length = payload.len() + 4; + let mut body = format!("{length:04x}{payload}0000").into_bytes(); + body.extend_from_slice(b"PACK"); + let mut file = file_with_bytes(&body); + let parsed = ReceivePackRequest::parse_file(&mut file).unwrap(); + assert_eq!(parsed.updates[0].ref_name, "refs/tags/v1"); + } + + #[test] + fn resolves_a_thin_pack_from_the_upstream_repository() { + let fixture = TempDir::new().unwrap(); + let source = fixture.path().join("source.git"); + run_git( + None, + &["init", "--bare", repo_string(&source).unwrap()], + None, + ) + .unwrap(); + let blob = object(&source, "blob", b"base\n"); + let tree_line = format!("100644 blob {blob}\tREADME.md\n"); + let tree = String::from_utf8( + run_git(Some(&source), &["mktree"], Some(tree_line.as_bytes())).unwrap(), + ) + .unwrap() + .trim() + .to_string(); + let base = unsigned_commit(&source, &tree, None, "base"); + run_git( + Some(&source), + &["update-ref", "refs/heads/main", &base], + None, + ) + .unwrap(); + run_git( + Some(&source), + &["symbolic-ref", "HEAD", "refs/heads/main"], + None, + ) + .unwrap(); + let tip = unsigned_commit(&source, &tree, Some(&base), "tip"); + let revisions = format!("{tip}\n^{base}\n"); + let pack = run_git( + Some(&source), + &["pack-objects", "--stdout", "--revs", "--thin"], + Some(revisions.as_bytes()), + ) + .unwrap(); + let body = receive_pack_body(&tip, &pack); + + let key = fixture.path().join("signing-key"); + assert!( + Command::new("ssh-keygen") + .args(["-q", "-t", "ed25519", "-N", "", "-f"]) + .arg(&key) + .status() + .unwrap() + .success() + ); + let signed = GitSigner::new(key) + .unwrap() + .sign_receive_pack(file_with_bytes(&body), source.to_str(), &test_control()) + .unwrap(); + assert_eq!(signed.signed_commits, 1); + } + + fn file_with_bytes(bytes: &[u8]) -> File { + let mut file = tempfile::tempfile().unwrap(); + file.write_all(bytes).unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + file + } + + fn test_control() -> SignControl { + SignControl::new( + Arc::new(AtomicBool::new(false)), + Instant::now() + Duration::from_secs(300), + ) + } + + fn object(repo: &Path, kind: &str, body: &[u8]) -> String { + String::from_utf8( + run_git( + Some(repo), + &["hash-object", "-t", kind, "-w", "--stdin"], + Some(body), + ) + .unwrap(), + ) + .unwrap() + .trim() + .to_string() + } + + fn unsigned_commit(repo: &Path, tree: &str, parent: Option<&str>, subject: &str) -> String { + let parent = parent.map_or(String::new(), |oid| format!("parent {oid}\n")); + let raw = format!( + "tree {tree}\n{parent}author Agent 1700000000 +0000\ncommitter Agent 1700000000 +0000\n\n{subject}\n" + ); + object(repo, "commit", raw.as_bytes()) + } + + fn receive_pack_body(new_oid: &str, pack: &[u8]) -> Vec { + let payload = format!( + "{ZERO_SHA1} {new_oid} refs/heads/main\0 report-status side-band-64k object-format=sha1\n" + ); + let length = payload.len() + 4; + let mut body = format!("{length:04x}{payload}0000").into_bytes(); + body.extend_from_slice(pack); + body + } + + fn pseudo_random_bytes(len: usize) -> Vec { + let mut state = 0x4d59_5df4_d0f3_3173_u64; + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state as u8 + }) + .collect() + } + + fn verify_commit(repo: &Path, allowed: &Path, oid: &str) { + let output = Command::new("git") + .arg("-C") + .arg(repo) + .args(["-c", "gpg.format=ssh", "-c"]) + .arg(format!("gpg.ssh.allowedSignersFile={}", allowed.display())) + .args(["verify-commit", oid]) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/tasks/rust.toml b/tasks/rust.toml index 89f2061bc6..7e0e6cff04 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -32,6 +32,7 @@ run = [ "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings", + "cargo clippy --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/isolation-backend-v8/Cargo.toml --all-targets -- -D warnings", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 lint native" @@ -44,6 +45,7 @@ run = [ "cargo fmt --manifest-path e2e/rust/Cargo.toml --all", "cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all", "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all", + "cargo fmt --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml --all", "cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all", ] hide = true @@ -55,6 +57,7 @@ run = [ "cargo fmt --manifest-path e2e/rust/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/governance-interceptor/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all -- --check", + "cargo fmt --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml --all -- --check", "cargo fmt --manifest-path examples/isolation-backend-v8/Cargo.toml --all -- --check", ] hide = true diff --git a/tasks/test.toml b/tasks/test.toml index ff2a2f4d02..e3cc42b46d 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -119,6 +119,7 @@ run = [ "cargo test --workspace --exclude openshell-server", "cargo test -p openshell-server --features test-support", "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml", + "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-git-signing/Cargo.toml", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true