From f5784ab065c4e64c400174fbf0d2771e159f3a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 06:26:15 +0200 Subject: [PATCH 1/8] feat(observability): supervisor OTLP telemetry relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an OTLP relay to the sandbox supervisor that accepts agent-emitted traces on 127.0.0.1:4318, enriches spans with sandbox resource attributes, and forwards them to the gateway over the existing session protocol. The relay enables zero-config observability for OTel-instrumented agents inside network-isolated sandboxes. Agents export to the standard OTLP endpoint; the supervisor handles enrichment, buffering, and forwarding without requiring egress policy exceptions. Key components: - OTLP HTTP receiver (protobuf + JSON) with per-driver binding (netns for Docker/Podman, direct for K8s/VM) - Span enrichment with sandbox identity (sandbox_id, workspace_id, policy, user, image, driver) and telemetry source marker - Bounded buffer (4096 slots) with non-blocking try_send forwarding - Gateway-side TelemetryRelayExporter with dedicated gRPC client that preserves supervisor-enriched resource attributes - Capability negotiation: supervisor advertises telemetry_relay, gateway confirms only when OTLP export is configured - Graceful shutdown ordering: relay drains before network teardown Proto: TelemetryData message, capabilities on SupervisorHello and SessionAccepted. Closes: #2641 Signed-off-by: Roland Huß --- Cargo.lock | 1838 ++++++++--------- Cargo.toml | 1 + architecture/sandbox.md | 66 + crates/openshell-core/src/sandbox_env.rs | 12 + .../openshell-driver-kubernetes/src/driver.rs | 10 + .../openshell-driver-podman/src/container.rs | 612 +----- crates/openshell-ocsf/src/lib.rs | 3 +- .../openshell-ocsf/src/tracing_layers/mod.rs | 2 + .../src/tracing_layers/relay_layer.rs | 46 + crates/openshell-otel/src/lib.rs | 2 +- crates/openshell-otel/src/propagation.rs | 25 + crates/openshell-sandbox/src/lib.rs | 71 + crates/openshell-server/Cargo.toml | 1 + crates/openshell-server/src/lib.rs | 8 + .../src/supervisor_session.rs | 65 +- .../openshell-server/src/telemetry_relay.rs | 79 + .../openshell-supervisor-network/Cargo.toml | 6 + .../openshell-supervisor-network/src/lib.rs | 1 + .../src/otlp/buffer.rs | 152 ++ .../src/otlp/enrichment.rs | 155 ++ .../src/otlp/mod.rs | 291 +++ .../src/otlp/receiver.rs | 177 ++ .../openshell-supervisor-network/src/proxy.rs | 13 + .../src/child_env.rs | 13 + .../src/process.rs | 7 + .../openshell-supervisor-process/src/run.rs | 2 + .../openshell-supervisor-process/src/ssh.rs | 3 + .../src/supervisor_session.rs | 29 +- mise.lock | 16 + proto/openshell.proto | 21 + 30 files changed, 2156 insertions(+), 1571 deletions(-) create mode 100644 crates/openshell-ocsf/src/tracing_layers/relay_layer.rs create mode 100644 crates/openshell-server/src/telemetry_relay.rs create mode 100644 crates/openshell-supervisor-network/src/otlp/buffer.rs create mode 100644 crates/openshell-supervisor-network/src/otlp/enrichment.rs create mode 100644 crates/openshell-supervisor-network/src/otlp/mod.rs create mode 100644 crates/openshell-supervisor-network/src/otlp/receiver.rs diff --git a/Cargo.lock b/Cargo.lock index edc940ea68..947b67ae32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,28 +29,28 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "zeroize", ] [[package]] name = "aes-gcm" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" dependencies = [ "aead", "aes", "cipher", "ctr", + "ctutils", "ghash", - "subtle", "zeroize", ] @@ -69,9 +69,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -84,9 +84,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -127,7 +127,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -138,44 +138,44 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "apollo-parser" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0" +checksum = "11f6e2be4b3474e5890d34d3e483d49ec9b5cbf9e358bc62c9cc5423376df54b" dependencies = [ "memchr", "rowan", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] [[package]] name = "argon2" -version = "0.6.0-rc.8" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "password-hash", ] @@ -203,7 +203,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -215,7 +215,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -230,13 +230,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -256,15 +256,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.8.15" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +checksum = "a767267da9e2c2e189b2f9df8b5657e850ecf5352644734ba130d4a57095cf1b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -274,11 +274,12 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", - "http 1.4.0", + "http 1.5.0", "time", "tokio", "tracing", @@ -287,9 +288,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -299,9 +300,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -310,9 +311,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -323,9 +324,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -338,8 +339,8 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "percent-encoding", "pin-project-lite", "tracing", @@ -348,10 +349,11 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.102.0" +version = "1.113.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc35b7a14cabdad13795fbbbd26d5ddec0882c01492ceedf2af575aad5f37dd" +checksum = "68182ecb449f7537db0f4d5d25917789cf41e32074a9fe47b6a0b847fe1d2032" dependencies = [ + "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", @@ -361,21 +363,22 @@ dependencies = [ "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-smithy-xml", "aws-types", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.5.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.4.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" dependencies = [ "aws-credential-types", "aws-smithy-http", @@ -384,20 +387,20 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac 0.12.1", + "hmac 0.13.0", "http 0.2.12", - "http 1.4.0", + "http 1.5.0", "percent-encoding", - "sha2 0.10.9", + "sha2 0.11.0", "time", "tracing", ] [[package]] name = "aws-smithy-async" -version = "1.2.14" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" dependencies = [ "futures-util", "pin-project-lite", @@ -406,9 +409,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.6" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -416,8 +419,8 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "percent-encoding", "pin-project-lite", @@ -427,15 +430,15 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.13" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3ef8931ad1c98aa6a55b4256f847f3116090819844e0dd41ea682cac5dd2d3" +checksum = "ebfd138fac0337cee7516c352757ea73b9f2266e57d0bcb5bc70e9547e45aef1" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2", - "http 1.4.0", + "http 1.5.0", "hyper", "hyper-rustls", "hyper-util", @@ -451,9 +454,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.7" +version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-schema", @@ -462,28 +465,31 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", + "aws-smithy-xml", "urlencoding", ] [[package]] name = "aws-smithy-runtime" -version = "1.11.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -495,9 +501,9 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.5.0", "http-body 0.4.6", - "http-body 1.0.1", + "http-body 1.1.0", "http-body-util", "pin-project-lite", "pin-utils", @@ -507,16 +513,16 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.12.3" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" +checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.5.0", "pin-project-lite", "tokio", "tracing", @@ -525,40 +531,40 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "aws-smithy-schema" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.0", + "http 1.5.0", ] [[package]] name = "aws-smithy-types" -version = "1.5.0" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" +checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.4.0", + "http 1.5.0", "http-body 0.4.6", - "http-body 1.0.1", + "http-body 1.1.0", "http-body-util", "itoa", "num-integer", @@ -573,18 +579,21 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.15" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.16" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -606,8 +615,8 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-util", @@ -621,7 +630,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1 0.10.6", + "sha1 0.10.7", "sync_wrapper", "tokio", "tokio-tungstenite 0.29.0", @@ -639,8 +648,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "mime", "pin-project-lite", @@ -658,7 +667,7 @@ checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "getrandom 0.2.17", "instant", - "rand 0.8.6", + "rand 0.8.8", ] [[package]] @@ -670,7 +679,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", "windows-link", @@ -744,20 +753,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] [[package]] name = "blake2" -version = "0.11.0-rc.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -766,14 +775,14 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", ] [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", "zeroize", @@ -810,7 +819,7 @@ dependencies = [ "futures-core", "futures-util", "hex", - "http 1.4.0", + "http 1.5.0", "http-body-util", "hyper", "hyper-named-pipe", @@ -823,7 +832,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_urlencoded", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-util", "tower-service", @@ -844,19 +853,19 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -866,9 +875,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytes-utils" @@ -917,9 +926,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "jobserver", @@ -927,12 +936,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -941,28 +944,28 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", "zeroize", ] [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -978,7 +981,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", "inout", "zeroize", @@ -986,9 +989,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -996,9 +999,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -1008,9 +1011,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" dependencies = [ "clap", "clap_lex", @@ -1020,14 +1023,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -1059,9 +1062,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -1102,6 +1105,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1174,9 +1189,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -1192,15 +1207,15 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -1213,36 +1228,36 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -1250,7 +1265,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "crossterm_winapi", "libc", "mio 0.8.11", @@ -1266,9 +1281,9 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "crossterm_winapi", - "mio 1.2.0", + "mio 1.2.3", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -1291,7 +1306,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", "rand_core 0.6.4", "subtle", "zeroize", @@ -1305,7 +1320,7 @@ checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", "ctutils", - "getrandom 0.4.2", + "getrandom 0.4.3", "hybrid-array", "num-traits", "rand_core 0.10.1", @@ -1316,11 +1331,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", "typenum", ] @@ -1330,7 +1345,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "hybrid-array", "rand_core 0.10.1", ] @@ -1387,9 +1402,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "curve25519-dalek-derive", - "digest 0.11.2", + "digest 0.11.3", "fiat-crypto 0.3.0", "rand_core 0.10.1", "rustc_version", @@ -1405,7 +1420,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1429,7 +1444,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1440,14 +1455,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "deadpool" @@ -1475,7 +1490,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1491,9 +1506,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid 0.10.2", "pem-rfc7468 1.0.0", @@ -1519,9 +1534,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derivative" @@ -1552,7 +1564,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1562,7 +1574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1595,17 +1607,17 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common 0.1.7", + "crypto-common 0.1.6", "subtle", ] [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", @@ -1613,13 +1625,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -1660,8 +1672,8 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der 0.8.0", - "digest 0.11.2", + "der 0.8.1", + "digest 0.11.3", "elliptic-curve 0.14.1", "rfc6979 0.6.0", "signature 3.0.0", @@ -1721,9 +1733,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -1738,7 +1750,7 @@ dependencies = [ "crypto-bigint 0.5.5", "digest 0.10.7", "ff 0.13.1", - "generic-array 0.14.7", + "generic-array 0.14.9", "group 0.13.0", "hkdf 0.12.4", "pem-rfc7468 0.7.0", @@ -1758,7 +1770,7 @@ dependencies = [ "base16ct 1.0.0", "crypto-bigint 0.7.5", "crypto-common 0.2.2", - "digest 0.11.2", + "digest 0.11.3", "ff 0.14.0", "group 0.14.0", "hkdf 0.13.0", @@ -1786,7 +1798,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1806,7 +1818,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1822,7 +1834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1838,20 +1850,30 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] +[[package]] +name = "evmap" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8" +dependencies = [ + "hashbag", + "left-right", + "smallvec", +] + [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "ff" @@ -1887,20 +1909,19 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fixedbitset" @@ -1910,12 +1931,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -1973,9 +1995,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1988,9 +2010,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1998,15 +2020,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -2026,38 +2048,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2070,11 +2092,26 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -2083,11 +2120,11 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.3.5" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", "rustversion", "typenum", ] @@ -2119,30 +2156,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] [[package]] name = "getset" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" dependencies = [ - "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2152,6 +2186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval", + "zeroize", ] [[package]] @@ -2162,15 +2197,15 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -2211,7 +2246,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.5.0", "indexmap", "slab", "tokio", @@ -2219,6 +2254,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbag" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2251,9 +2292,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -2272,9 +2313,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2290,18 +2331,18 @@ checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hickory-proto" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d" dependencies = [ "data-encoding", "idna", "ipnet", - "jni 0.22.4", + "jni", "once_cell", "rand 0.10.2", "ring", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "url", @@ -2340,7 +2381,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -2365,9 +2406,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2395,24 +2436,24 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.4.0", + "http 1.5.0", ] [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "pin-project-lite", ] @@ -2442,17 +2483,17 @@ dependencies = [ [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "httparse", "httpdate", "itoa", @@ -2464,9 +2505,9 @@ dependencies = [ [[package]] name = "hyper-named-pipe" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" dependencies = [ "hex", "hyper", @@ -2474,7 +2515,6 @@ dependencies = [ "pin-project-lite", "tokio", "tower-service", - "winapi", ] [[package]] @@ -2483,7 +2523,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.5.0", "hyper", "hyper-util", "log", @@ -2517,8 +2557,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "hyper", "ipnet", "libc", @@ -2571,9 +2611,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2585,9 +2625,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2598,9 +2638,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2612,16 +2652,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2632,15 +2673,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2651,12 +2692,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2676,9 +2711,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2705,14 +2740,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] @@ -2730,20 +2763,20 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "inotify-sys", "libc", ] [[package]] name = "inotify-sys" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" dependencies = [ "libc", ] @@ -2781,19 +2814,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_ci" @@ -2803,11 +2826,11 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_executable" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2844,26 +2867,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.18" +name = "itertools" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] [[package]] -name = "jni" -version = "0.21.1" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -2874,10 +2890,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -2892,16 +2908,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", + "syn 2.0.119", ] [[package]] @@ -2920,28 +2927,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2973,9 +2979,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "aws-lc-rs", "base64 0.22.1", @@ -2986,13 +2992,14 @@ dependencies = [ "p256 0.13.2", "p384 0.13.1", "pem", - "rand 0.8.6", + "rand 0.8.8", "rsa 0.9.10", "serde", "serde_json", "sha2 0.10.9", "signature 2.2.0", "simple_asn1", + "zeroize", ] [[package]] @@ -3010,12 +3017,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -3045,9 +3052,9 @@ checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" [[package]] name = "kqueue" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" dependencies = [ "kqueue-sys", "libc", @@ -3059,7 +3066,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "libc", ] @@ -3088,8 +3095,8 @@ dependencies = [ "either", "futures", "home", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-rustls", @@ -3121,7 +3128,7 @@ checksum = "7845bcc3e0f422df4d9049570baedd9bc1942f0504594e393e72fe24092559cf" dependencies = [ "chrono", "form_urlencoded", - "http 1.4.0", + "http 1.5.0", "json-patch", "k8s-openapi", "schemars", @@ -3140,7 +3147,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3171,13 +3178,13 @@ dependencies = [ [[package]] name = "landlock" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088" +checksum = "4cca98e95f35b29d469dade6724c6f96cec9236640f745a0e99b0334ec320ab1" dependencies = [ "enumflags2", "libc", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -3190,10 +3197,15 @@ dependencies = [ ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "left-right" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "8bc015ded5d9b3054dbbdb63332cdd6ee42352ccef19e911e25117490e2f48ee" +dependencies = [ + "crossbeam-utils", + "loom", + "slab", +] [[package]] name = "libc" @@ -3219,14 +3231,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.7.4", + "redox_syscall 0.9.3", ] [[package]] @@ -3264,9 +3276,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -3279,9 +3291,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] [[package]] name = "lru" @@ -3331,27 +3356,28 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.18.1" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" +checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108" dependencies = [ "base64 0.22.1", + "evmap", "http-body-util", "hyper", "hyper-util", @@ -3360,24 +3386,25 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", ] [[package]] name = "metrics-util" -version = "0.20.1" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" +checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13" dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.16.1", "metrics", "quanta", - "rand 0.9.4", + "rand 0.9.5", "rand_xoshiro", + "rapidhash", "sketches-ddsketch", ] @@ -3408,7 +3435,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -3428,6 +3455,15 @@ name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -3447,9 +3483,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -3503,7 +3539,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3515,7 +3551,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3537,13 +3573,13 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", "libc", "log", - "mio 1.2.0", + "mio 1.2.3", "notify-types", "walkdir", "windows-sys 0.60.2", @@ -3555,7 +3591,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] @@ -3564,14 +3600,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -3588,33 +3624,32 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand 0.8.8", "smallvec", "zeroize", ] [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -3651,11 +3686,11 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.0", - "rand 0.8.6", + "http 1.5.0", + "rand 0.8.8", "reqwest 0.12.28", "serde", "serde_json", @@ -3683,18 +3718,18 @@ dependencies = [ "bytes", "chrono", "futures-util", - "http 1.4.0", + "http 1.5.0", "http-auth", "jsonwebtoken", "lazy_static", "oci-spec", "olpc-cjson", "regex", - "reqwest 0.13.2", + "reqwest 0.13.4", "serde", "serde_json", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "unicase", @@ -3714,7 +3749,7 @@ dependencies = [ "serde_json", "strum 0.27.2", "strum_macros 0.27.2", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -3812,7 +3847,7 @@ dependencies = [ "tar", "temp-env", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-stream", @@ -3828,7 +3863,7 @@ dependencies = [ name = "openshell-conformance" version = "0.0.0" dependencies = [ - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", "tempfile", @@ -3871,7 +3906,7 @@ dependencies = [ "serde_json", "tar", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tonic", @@ -3907,7 +3942,7 @@ dependencies = [ "bytes", "clap", "futures", - "http 1.4.0", + "http 1.5.0", "miette", "openshell-core", "openshell-otel", @@ -3924,7 +3959,7 @@ dependencies = [ "tokio-stream", "toml", "tonic", - "tower-http 0.6.8", + "tower-http 0.6.11", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -3938,7 +3973,7 @@ dependencies = [ "bytes", "clap", "futures", - "http 1.4.0", + "http 1.5.0", "http-body-util", "k8s-openapi", "kube", @@ -3956,7 +3991,7 @@ dependencies = [ "serde", "serde_json", "temp-env", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "toml", @@ -3997,7 +4032,7 @@ dependencies = [ "serde_json", "serde_yml", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tonic", @@ -4011,7 +4046,7 @@ version = "0.0.0" dependencies = [ "clap", "futures", - "http 1.4.0", + "http 1.5.0", "http-body-util", "hyper", "hyper-util", @@ -4027,11 +4062,11 @@ dependencies = [ "serde", "serde_json", "temp-env", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tonic", - "tower-http 0.6.8", + "tower-http 0.6.11", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -4067,7 +4102,7 @@ dependencies = [ "clap", "flate2", "futures", - "http 1.4.0", + "http 1.5.0", "libc", "libloading", "miette", @@ -4094,7 +4129,7 @@ dependencies = [ "tokio", "tokio-stream", "tonic", - "tower-http 0.6.8", + "tower-http 0.6.11", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -4106,13 +4141,13 @@ dependencies = [ name = "openshell-extension-core" version = "0.0.0" dependencies = [ - "http 1.4.0", + "http 1.5.0", "hyper-util", "rcgen", "rustls", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tonic", @@ -4156,7 +4191,7 @@ dependencies = [ "prost-types", "serde_json", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tonic", "tracing", @@ -4180,14 +4215,14 @@ name = "openshell-otel" version = "0.0.0" dependencies = [ "futures", - "http 1.4.0", + "http 1.5.0", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tonic", - "tower-http 0.6.8", + "tower-http 0.6.11", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -4242,7 +4277,7 @@ dependencies = [ "serde", "serde_json", "serde_yml", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", ] @@ -4257,7 +4292,7 @@ dependencies = [ "serde_json", "serde_yml", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "uuid", @@ -4311,7 +4346,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-stream", @@ -4340,8 +4375,8 @@ dependencies = [ "glob", "hex", "hmac 0.12.1", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-rustls", @@ -4371,13 +4406,14 @@ dependencies = [ "openshell-supervisor-middleware", "openshell-supervisor-middleware-builtins", "opentelemetry", + "opentelemetry-proto", "opentelemetry_sdk", "petname", "pin-project-lite", "prost", "prost-reflect", "prost-types", - "rand 0.9.4", + "rand 0.9.5", "rcgen", "reqwest 0.12.28", "ring", @@ -4393,7 +4429,7 @@ dependencies = [ "spiffe", "sqlx", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-stream", @@ -4401,7 +4437,7 @@ dependencies = [ "toml", "tonic", "tower 0.5.3", - "tower-http 0.6.8", + "tower-http 0.6.11", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -4417,7 +4453,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4467,16 +4503,22 @@ dependencies = [ "glob", "hex", "hickory-proto", - "http 1.4.0", - "ipnet", - "libc", + "http 1.5.0", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "libc", "miette", "openshell-core", "openshell-ocsf", + "openshell-otel", "openshell-policy", "openshell-router", "openshell-supervisor-middleware", "openshell-supervisor-middleware-builtins", + "opentelemetry-proto", + "prost", "prost-types", "rcgen", "regorus", @@ -4487,12 +4529,12 @@ dependencies = [ "serde", "serde_json", "serde_yml", - "sha1 0.10.6", + "sha1 0.10.7", "sha2 0.10.9", "spiffe", "temp-env", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-stream", @@ -4566,7 +4608,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", ] @@ -4580,7 +4622,7 @@ dependencies = [ "once_cell", "shell-escape", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", ] @@ -4600,7 +4642,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", ] @@ -4610,12 +4652,12 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ - "http 1.4.0", + "http 1.5.0", "opentelemetry", "opentelemetry-proto", "opentelemetry_sdk", "prost", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tonic", "tonic-types", @@ -4627,9 +4669,12 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ + "base64 0.22.1", + "const-hex", "opentelemetry", "opentelemetry_sdk", "prost", + "serde", "tonic", "tonic-prost", ] @@ -4646,8 +4691,8 @@ dependencies = [ "opentelemetry", "percent-encoding", "portable-atomic", - "rand 0.9.4", - "thiserror 2.0.18", + "rand 0.9.5", + "thiserror 2.0.20", "tokio", "tokio-stream", ] @@ -4669,9 +4714,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "owo-colors" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "p256" @@ -4740,18 +4785,19 @@ dependencies = [ [[package]] name = "pageant" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" +checksum = "6d8eab09a361a4afe0b1668be978cd97e4f052e927a92b0b608cf902965d49ce" dependencies = [ + "base16ct 1.0.0", "byteorder", "bytes", "delegate", "futures", "log", - "rand 0.8.6", - "sha2 0.10.9", - "thiserror 1.0.69", + "rand 0.10.2", + "sha2 0.11.0", + "thiserror 2.0.20", "tokio", "windows", "windows-strings", @@ -4807,7 +4853,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "hmac 0.13.0", ] @@ -4847,9 +4893,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -4857,9 +4903,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -4867,25 +4913,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -4907,10 +4952,10 @@ checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", "clap", - "itertools 0.14.0", + "itertools 0.15.0", "proc-macro2", "quote", - "rand 0.8.6", + "rand 0.8.8", ] [[package]] @@ -4925,22 +4970,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4972,7 +5017,7 @@ version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ - "der 0.8.0", + "der 0.8.1", "spki 0.8.0", ] @@ -4985,7 +5030,7 @@ dependencies = [ "aes", "aes-gcm", "cbc", - "der 0.8.0", + "der 0.8.1", "pbkdf2", "rand_core 0.10.1", "scrypt", @@ -5009,7 +5054,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.0", + "der 0.8.1", "pkcs5", "rand_core 0.10.1", "spki 0.8.0", @@ -5017,9 +5062,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plain" @@ -5047,7 +5092,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "universal-hash", "zeroize", ] @@ -5059,21 +5104,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "universal-hash", + "zeroize", ] [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -5100,7 +5146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5140,41 +5186,34 @@ dependencies = [ ] [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" +name = "proc-macro2" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", + "unicode-ident", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "proptest" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "unicode-ident", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", ] [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -5182,9 +5221,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools 0.14.0", @@ -5197,21 +5236,21 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.117", + "syn 2.0.119", "tempfile", ] [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5229,9 +5268,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -5302,20 +5341,20 @@ checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" [[package]] name = "pulldown-cmark" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "memchr", "unicase", ] [[package]] name = "pulldown-cmark-to-cmark" -version = "22.0.0" +version = "22.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60" dependencies = [ "pulldown-cmark", ] @@ -5337,19 +5376,19 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -5363,16 +5402,16 @@ checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.4.2", + "getrandom 0.4.3", "lru-slab", "rand 0.10.2", "rand_pcg", "ring", - "rustc-hash 2.1.2", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -5380,23 +5419,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -5415,9 +5454,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5426,9 +5465,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5441,7 +5480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -5498,6 +5537,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -5507,13 +5555,22 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + [[package]] name = "ratatui" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "cassowary", "compact_str", "crossterm 0.27.0", @@ -5533,7 +5590,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] @@ -5555,23 +5612,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] name = "redox_syscall" -version = "0.7.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -5581,9 +5638,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -5598,9 +5655,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "regorus" @@ -5614,11 +5671,11 @@ dependencies = [ "msvc_spectre_libs", "num-bigint", "num-traits", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", "spin", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -5632,8 +5689,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-rustls", @@ -5653,7 +5710,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower 0.5.3", - "tower-http 0.6.8", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", @@ -5663,16 +5720,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", "futures-core", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-rustls", @@ -5693,7 +5750,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower 0.5.3", - "tower-http 0.6.8", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", @@ -5777,7 +5834,7 @@ dependencies = [ "const-oid 0.10.2", "crypto-bigint 0.7.5", "crypto-primes", - "digest 0.11.2", + "digest 0.11.3", "pkcs1 0.8.0-rc.4", "pkcs8 0.11.0", "rand_core 0.10.1", @@ -5789,13 +5846,13 @@ dependencies = [ [[package]] name = "russh" -version = "0.62.5" +version = "0.62.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c230e0ed9cbeb92fbad6c8848985d6df2a1464c0dc247a021abd666e9005e" +checksum = "9decb68e4e44e1079700e54f17c8f23806ec53d7e0db73ab1c71d9dabc666812" dependencies = [ "aes", "aws-lc-rs", - "bitflags 2.11.1", + "bitflags 2.13.1", "block-padding", "byteorder", "bytes", @@ -5806,16 +5863,16 @@ dependencies = [ "curve25519-dalek 5.0.0", "data-encoding", "delegate", - "der 0.8.0", - "digest 0.11.2", + "der 0.8.1", + "digest 0.11.3", "ecdsa 0.17.0", "ed25519-dalek 3.0.0", "elliptic-curve 0.14.1", "enum_dispatch", "flate2", "futures", - "generic-array 1.3.5", - "getrandom 0.4.2", + "generic-array 1.4.5", + "getrandom 0.4.3", "ghash", "hex-literal", "hmac 0.13.0", @@ -5852,7 +5909,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "typenum", "universal-hash", @@ -5885,9 +5942,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" @@ -5897,9 +5954,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -5925,7 +5982,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5938,18 +5995,18 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -5963,9 +6020,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -5984,9 +6041,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -5994,13 +6051,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -6010,7 +6067,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6021,9 +6078,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -6033,9 +6090,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -6092,9 +6149,15 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.119", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6121,7 +6184,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", "der 0.7.10", - "generic-array 0.14.7", + "generic-array 0.14.9", "pkcs8 0.10.2", "subtle", "zeroize", @@ -6135,7 +6198,7 @@ checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct 1.0.0", "ctutils", - "der 0.8.0", + "der 0.8.1", "hybrid-array", "subtle", "zeroize", @@ -6166,7 +6229,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -6191,9 +6254,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -6211,22 +6274,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -6237,14 +6300,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -6266,13 +6329,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] @@ -6326,9 +6389,9 @@ dependencies = [ [[package]] name = "serdect" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" dependencies = [ "base16ct 1.0.0", "serde", @@ -6336,9 +6399,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -6352,8 +6415,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -6374,8 +6437,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -6384,7 +6447,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "keccak", ] @@ -6394,7 +6457,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -6422,9 +6485,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -6444,7 +6507,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", - "mio 1.2.0", + "mio 1.2.3", "signal-hook", ] @@ -6474,15 +6537,15 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "rand_core 0.10.1", ] [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" @@ -6508,7 +6571,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", ] @@ -6526,21 +6589,21 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6560,7 +6623,7 @@ dependencies = [ "prost-types", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tokio", "tokio-util", @@ -6574,9 +6637,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -6598,7 +6661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der 0.8.1", ] [[package]] @@ -6649,7 +6712,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tokio-stream", "tracing", @@ -6666,7 +6729,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6688,7 +6751,7 @@ dependencies = [ "sqlx-core", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.117", + "syn 2.0.119", "tokio", "url", ] @@ -6701,7 +6764,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.13.1", "byteorder", "bytes", "crc", @@ -6712,7 +6775,7 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "generic-array 0.14.7", + "generic-array 0.14.9", "hex", "hkdf 0.12.4", "hmac 0.12.1", @@ -6722,14 +6785,14 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.6", + "rand 0.8.8", "rsa 0.9.10", - "sha1 0.10.6", + "sha1 0.10.7", "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", "whoami", ] @@ -6742,7 +6805,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.13.1", "byteorder", "crc", "dotenvy", @@ -6759,14 +6822,14 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.6", + "rand 0.8.8", "serde", "serde_json", "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", "whoami", ] @@ -6790,7 +6853,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", "url", ] @@ -6823,7 +6886,7 @@ dependencies = [ "bytes", "crypto-bigint 0.7.5", "ctutils", - "digest 0.11.2", + "digest 0.11.3", "pem-rfc7468 1.0.0", "zeroize", ] @@ -6861,7 +6924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6918,7 +6981,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6930,7 +6993,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6979,9 +7042,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -7005,7 +7079,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -7035,10 +7109,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7050,7 +7124,7 @@ dependencies = [ "cfg-if", "libc", "memchr", - "mio 1.2.0", + "mio 1.2.3", "terminal-trx", "windows-sys 0.61.2", "xterm-color", @@ -7074,7 +7148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7104,11 +7178,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -7119,37 +7193,36 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -7159,15 +7232,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -7175,9 +7248,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -7185,9 +7258,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" dependencies = [ "tinyvec_macros", ] @@ -7200,13 +7273,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", - "mio 1.2.0", + "mio 1.2.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -7217,20 +7290,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -7238,9 +7311,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -7277,13 +7350,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "slab", "tokio", @@ -7341,8 +7415,8 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "hyper", "hyper-timeout", @@ -7370,7 +7444,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -7395,7 +7469,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.117", + "syn 2.0.119", "tempfile", "tonic-build", ] @@ -7454,10 +7528,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "base64 0.21.7", - "bitflags 2.11.1", + "bitflags 2.13.1", "bytes", - "http 1.4.0", - "http-body 1.0.1", + "http 1.5.0", + "http-body 1.1.0", "http-body-util", "mime", "pin-project-lite", @@ -7468,21 +7542,21 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.1", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "iri-string", + "http 1.5.0", + "http-body 1.1.0", "pin-project-lite", "tower 0.5.3", "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -7494,14 +7568,14 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-mcp-types" -version = "0.12.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6511f1f32c7cb7fd4525edc0eb4dcf307db8f7eceb2833ab24a37b4cc10cda61" +checksum = "22ba370da8217bdfc0450983450cdb9dcdd3a11fba6e49802b265c3f0ca266d1" dependencies = [ "base64 0.22.1", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -7530,7 +7604,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -7543,7 +7617,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -7627,14 +7701,14 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.5.0", "httparse", "log", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-pki-types", - "sha1 0.10.6", - "thiserror 2.0.18", + "sha1 0.10.7", + "thiserror 2.0.20", "utf-8", ] @@ -7646,12 +7720,12 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.5.0", "httparse", "log", - "rand 0.9.4", - "sha1 0.10.6", - "thiserror 2.0.18", + "rand 0.9.5", + "sha1 0.10.7", + "thiserror 2.0.20", ] [[package]] @@ -7666,6 +7740,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.9.0" @@ -7707,9 +7787,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -7807,11 +7887,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -7867,20 +7947,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -7891,9 +7962,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -7904,9 +7975,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -7914,9 +7985,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7924,48 +7995,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -7979,23 +8028,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -8013,18 +8050,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -8061,7 +8098,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -8123,7 +8160,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -8134,7 +8171,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -8171,15 +8208,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -8225,21 +8253,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -8297,12 +8310,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8321,12 +8328,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8345,12 +8346,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8381,12 +8376,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8405,12 +8394,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8429,12 +8412,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8453,12 +8430,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8496,7 +8467,7 @@ dependencies = [ "base64 0.22.1", "deadpool", "futures", - "http 1.4.0", + "http 1.5.0", "http-body-util", "hyper", "hyper-util", @@ -8509,116 +8480,29 @@ dependencies = [ "url", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "wnaf" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" dependencies = [ "ff 0.14.0", "group 0.14.0", "hybrid-array", + "primefield", ] [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x509-parser" @@ -8670,9 +8554,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8687,7 +8571,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -8722,29 +8606,29 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -8757,7 +8641,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -8778,14 +8662,14 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -8794,9 +8678,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -8805,20 +8689,26 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.4", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" @@ -8831,9 +8721,9 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 6936439a3b..186f11de6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ tracing-appender = "0.2" opentelemetry = "0.32" opentelemetry_sdk = { version = "0.32", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.32", default-features = false, features = ["grpc-tonic", "trace"] } +opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic", "trace", "with-serde"] } tracing-opentelemetry = { version = "0.33", default-features = false, features = ["tracing-log"] } # Metrics diff --git a/architecture/sandbox.md b/architecture/sandbox.md index ba5b3c59b0..5847d6e316 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -462,6 +462,72 @@ Sandbox logs are emitted locally and can also be pushed back to the gateway. Security-relevant sandbox behavior uses OCSF structured events; internal diagnostics use ordinary tracing. +## Telemetry Relay + +The supervisor can relay OpenTelemetry trace data and OCSF events from agent +processes to the gateway over the session protocol. This gives OTel-instrumented +agents (LangChain, CrewAI, etc.) a zero-configuration path to an external +collector without requiring direct egress from the sandbox. + +### Data Flow + +``` +Agent process --> OTLP HTTP (127.0.0.1:4318) --> Supervisor receiver + --> Enrichment (sandbox resource attributes) + --> Bounded buffer (4096 slots, shared traces + OCSF) + --> Forwarder --> Session channel (TelemetryData message) + --> Gateway --> Dedicated SpanExporter --> External OTLP collector +``` + +### Receiver Binding + +The OTLP HTTP receiver always binds to `127.0.0.1:4318`. For Docker/Podman +drivers where the supervisor creates a network namespace, the bind happens +inside the namespace via `bind_tcp_in_netns()`. For Kubernetes and VM drivers, +the supervisor and agent share the same network namespace, so a direct bind +suffices. The receiver accepts both `application/x-protobuf` and +`application/json` content types. + +### Span Enrichment + +Forwarded spans are enriched with sandbox resource attributes: +`openshell.sandbox.id`, `openshell.workspace.id`, `openshell.sandbox.policy`, +`openshell.sandbox.user`, `openshell.sandbox.image`, `openshell.sandbox.driver`. +The `openshell.telemetry.source` attribute (fixed value `"agent"`) is always +injected regardless of the enrichment toggle so collectors can filter agent +spans from gateway infrastructure spans. Enrichment can be disabled for +pass-through forwarding. + +### Activation and Capability Negotiation + +The relay is opt-in. It starts only when the gateway has `[openshell.gateway.otlp]` +configured. The supervisor advertises `"telemetry_relay"` in +`SupervisorHello.capabilities`. The gateway confirms via `SessionAccepted.capabilities`. +The supervisor gates `TelemetryData` sending on this confirmation. When the +relay is active, the supervisor sets `OTEL_EXPORTER_OTLP_ENDPOINT` and +`OTEL_EXPORTER_OTLP_PROTOCOL` in agent child processes via `child_env.rs`. + +### Non-Interference + +The buffer uses `try_send` (non-blocking) on the session channel. When the +buffer reaches capacity, the oldest entries are dropped and a counter records +each drop. A queue depth gauge tracks buffer pressure. This ensures telemetry +cannot block or degrade sandbox control operations. + +### OCSF Event Relay + +OCSF events generated inside the sandbox (e.g., network deny events) can also +be forwarded through the same transport. A per-sandbox token bucket rate limiter +controls the OCSF event rate, with configurable rate and drop counter. + +### Gateway-Side Handling + +The gateway receives `TelemetryData` messages and exports trace data through a +dedicated `TelemetryRelayExporter` that connects directly to the configured OTLP +collector. This bypasses the gateway's own `SdkTracerProvider` to preserve the +supervisor-enriched resource attributes. OCSF events are emitted via +`tracing::info!` on the `ocsf_relay` target. + ## Policy Proposals When an L4 CONNECT is denied, the proxy emits a `DenialEvent`. The denial diff --git a/crates/openshell-core/src/sandbox_env.rs b/crates/openshell-core/src/sandbox_env.rs index 2ce8e4b058..ab93c4aadd 100644 --- a/crates/openshell-core/src/sandbox_env.rs +++ b/crates/openshell-core/src/sandbox_env.rs @@ -235,6 +235,18 @@ pub const SANDBOX_GID: &str = "OPENSHELL_SANDBOX_GID"; /// OCI only for the former contract. pub const OCI_IMAGE_USER: &str = "OPENSHELL_OCI_IMAGE_USER"; +/// Standard OpenTelemetry environment variable for the OTLP exporter endpoint. +/// +/// Set conditionally by the telemetry relay when the gateway has an OTLP +/// endpoint configured. Points agent SDKs at the supervisor's local OTLP +/// HTTP receiver. +pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; + +/// Standard OpenTelemetry environment variable for the OTLP exporter protocol. +/// +/// Set to `http/protobuf` when the telemetry relay is active. +pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; + // The corporate upstream-proxy configuration deliberately has no reserved // environment variables: it travels on the supervisor's argv // (`--upstream-proxy` and friends), which a sandbox image cannot forge the diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index bb1b75e8a9..d385f640f5 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -4341,6 +4341,16 @@ fn build_env_list( tls_enabled, provider_spiffe_socket_path, ); + upsert_env( + &mut env, + openshell_core::sandbox_env::OTEL_EXPORTER_OTLP_ENDPOINT, + "http://127.0.0.1:4318", + ); + upsert_env( + &mut env, + openshell_core::sandbox_env::OTEL_EXPORTER_OTLP_PROTOCOL, + "http/protobuf", + ); env } diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index a81ee13e1d..30e115ee48 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -14,6 +14,7 @@ use openshell_core::{driver_mounts, proto_struct}; use serde::Serialize; use serde_json::Value; use std::collections::{BTreeMap, HashSet}; +#[cfg(target_os = "linux")] use std::path::Path; /// Returns `true` when `SELinux` is enabled (enforcing or permissive). @@ -53,9 +54,6 @@ const VOLUME_PREFIX: &str = "openshell-sandbox-"; /// Secret name prefix for per-sandbox gateway JWTs. const TOKEN_SECRET_PREFIX: &str = "openshell-token-"; const PROXY_AUTH_SECRET_PREFIX: &str = "openshell-proxy-auth-"; -const TLS_CA_SECRET_PREFIX: &str = "openshell-tls-ca-"; -const TLS_CERT_SECRET_PREFIX: &str = "openshell-tls-cert-"; -const TLS_KEY_SECRET_PREFIX: &str = "openshell-tls-key-"; /// Container-side mount paths for client TLS materials and the sandbox token. const TLS_CA_MOUNT_PATH: &str = openshell_core::driver_utils::TLS_CA_MOUNT_PATH; @@ -64,9 +62,6 @@ const TLS_KEY_MOUNT_PATH: &str = openshell_core::driver_utils::TLS_KEY_MOUNT_PAT const SANDBOX_TOKEN_MOUNT_PATH: &str = openshell_core::driver_utils::SANDBOX_TOKEN_MOUNT_PATH; const UPSTREAM_PROXY_AUTH_MOUNT_PATH: &str = openshell_core::driver_utils::UPSTREAM_PROXY_AUTH_MOUNT_PATH; -const PROXY_CA_MOUNT_PATH: &str = openshell_core::driver_utils::PROXY_CA_MOUNT_PATH; -const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str = - openshell_core::driver_utils::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR; /// Directory inside sandbox containers where the supervisor binary is mounted. const SUPERVISOR_MOUNT_DIR: &str = openshell_core::driver_utils::SUPERVISOR_CONTAINER_DIR; @@ -176,16 +171,6 @@ pub fn proxy_auth_secret_name(sandbox_id: &str) -> String { format!("{PROXY_AUTH_SECRET_PREFIX}{sandbox_id}") } -/// Build per-sandbox Podman secret names for TLS CA, cert, and key. -#[must_use] -pub fn tls_secret_names(sandbox_id: &str) -> [String; 3] { - [ - format!("{TLS_CA_SECRET_PREFIX}{sandbox_id}"), - format!("{TLS_CERT_SECRET_PREFIX}{sandbox_id}"), - format!("{TLS_KEY_SECRET_PREFIX}{sandbox_id}"), - ] -} - /// Truncate a container ID to 12 characters (standard short form). #[must_use] pub fn short_id(id: &str) -> String { @@ -231,10 +216,6 @@ struct ContainerSpec { /// via Podman's `host-gateway` magic so sandbox containers can reach /// the gateway server running on the host in rootless mode. hostadd: Vec, - /// Search domains written to `/etc/resolv.conf` by Podman. - dns_search: Vec, - /// Resolver options written to `/etc/resolv.conf` by Podman. - dns_option: Vec, netns: NetNS, // Matches libpod's network spec format, which is `{name: {opts}}` where // empty opts is a unit struct rather than `()`. Keep as a map so JSON @@ -248,13 +229,6 @@ struct ContainerSpec { /// Port mappings from host to container. Using `host_port=0` requests an /// ephemeral port, readable back from the inspect response. portmappings: Vec, - /// User namespace mode override (e.g. `auto`). - #[serde(skip_serializing_if = "Option::is_none")] - userns: Option, - /// UID/GID mapping options. Required for `userns = "auto"` — the Podman - /// API needs `AutoUserNs: true` alongside the namespace mode. - #[serde(skip_serializing_if = "Option::is_none")] - idmappings: Option, } /// A port mapping entry for the libpod `SpecGenerator`. @@ -354,49 +328,6 @@ struct NetNS { nsmode: String, } -#[derive(Serialize)] -struct UserNS { - nsmode: String, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option, -} - -#[derive(Serialize)] -struct IDMap { - container_id: u32, - host_id: u32, - size: u32, -} - -#[derive(Serialize)] -struct IDMappings { - #[serde(rename = "HostUIDMapping")] - host_uid_mapping: bool, - #[serde(rename = "HostGIDMapping")] - host_gid_mapping: bool, - #[serde(rename = "AutoUserNs")] - auto_user_ns: bool, - #[serde(rename = "UIDMap", skip_serializing_if = "Vec::is_empty")] - uid_map: Vec, - #[serde(rename = "GIDMap", skip_serializing_if = "Vec::is_empty")] - gid_map: Vec, -} - -fn parse_id_maps(entries: &[String]) -> Result, ComputeDriverError> { - entries - .iter() - .map(|entry| { - let (cid, hid, size) = crate::config::parse_id_map_entry("idmap", entry) - .map_err(|err| ComputeDriverError::Precondition(err.to_string()))?; - Ok(IDMap { - container_id: cid, - host_id: hid, - size, - }) - }) - .collect() -} - #[derive(Serialize)] struct NetworkAttachment {} @@ -459,12 +390,6 @@ fn upstream_proxy_cli_args(config: &PodmanComputeConfig) -> Vec { if config.proxy_connect_by_hostname == Some(true) { args.push("--upstream-proxy-connect-by-hostname".to_string()); } - // A CA certificate is not secret, so the host PEM is bind-mounted - // read-only and the container-side mount path is passed on argv. - if config.proxy_ca_bundle.is_some() { - args.push("--upstream-proxy-ca-bundle".to_string()); - args.push(PROXY_CA_MOUNT_PATH.to_string()); - } args } @@ -529,23 +454,14 @@ fn build_env( config.sandbox_ssh_socket_path.clone(), ); env.insert("OPENSHELL_CONTAINER_IMAGE".into(), image.to_string()); - let main_process = openshell_core::sandbox_env::MainProcessConfig::encode_driver_spec(spec) - .expect("main process config serialization cannot fail"); env.insert( - openshell_core::sandbox_env::MAIN_PROCESS_SPEC.into(), - main_process, + openshell_core::sandbox_env::SANDBOX_COMMAND.into(), + "sleep infinity".into(), ); env.insert( openshell_core::sandbox_env::TELEMETRY_ENABLED.into(), openshell_core::telemetry::enabled_env_value().into(), ); - // Runtime capabilities are driver-owned. Override image/user input with - // only the substrate that this driver configures for the supervisor. - env.insert( - openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES.into(), - openshell_core::sandbox_env::POLICY_DNS_TRANSPARENT_TCP_CAPABILITY.into(), - ); - // 3. TLS client cert paths (when mTLS is enabled). These point to // the container-side mount paths where the cert files are // bind-mounted from the host. @@ -564,20 +480,8 @@ fn build_env( ); } - if let Some(socket_path) = provider_spiffe_workload_api_socket_env_value(config) { - env.insert( - openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET.into(), - socket_path, - ); - } - env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN); env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN_FILE); - // Prevent user-supplied environment from overriding the TLS server name - // the supervisor verifies — a sandbox user who can redirect the gateway - // hostname could otherwise present a certificate for a name they control - // and intercept the sandbox JWT. - env.remove(openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME); env.insert( openshell_core::sandbox_env::OCI_IMAGE_USER.into(), oci_user.to_string(), @@ -1000,12 +904,9 @@ pub fn build_container_spec_with_token_and_gpu_devices( image, image, "", - None, - None, ) } -#[allow(clippy::too_many_arguments)] pub fn build_container_spec_for_image( sandbox: &DriverSandbox, config: &PodmanComputeConfig, @@ -1014,8 +915,6 @@ pub fn build_container_spec_for_image( requested_image: &str, image_id: &str, oci_user: &str, - supervisor_bin_path: Option<&Path>, - tls_secret_names: Option<&[String; 3]>, ) -> Result { let name = container_name(&sandbox.workspace, &sandbox.name, &sandbox.id); let vol = volume_name(&sandbox.id); @@ -1057,15 +956,11 @@ pub fn build_container_spec_for_image( }]; volumes.extend(user_mounts.volumes); - let mut image_volumes = if supervisor_bin_path.is_some() { - Vec::new() - } else { - vec![ImageVolume { - source: config.supervisor_image.clone(), - destination: SUPERVISOR_MOUNT_DIR.into(), - rw: false, - }] - }; + let mut image_volumes = vec![ImageVolume { + source: config.supervisor_image.clone(), + destination: SUPERVISOR_MOUNT_DIR.into(), + rw: false, + }]; image_volumes.extend(user_mounts.image_volumes); let mut command = vec![ "--workdir".to_string(), @@ -1114,6 +1009,8 @@ pub fn build_container_spec_for_image( "DAC_OVERRIDE".into(), // Not needed: the supervisor does not create setuid/setgid executables. "FSETID".into(), + // Not needed: the supervisor does not send signals to arbitrary processes. + "KILL".into(), // Not needed: the supervisor does not bind privileged ports (<1024). "NET_BIND_SERVICE".into(), // Not in Podman's default set but explicitly denied in case the image @@ -1144,9 +1041,6 @@ pub fn build_container_spec_for_image( // Child setup clears the capability bounding set before exec, which // requires CAP_SETPCAP in the supervisor until drop_privileges(). "SETPCAP".into(), - // Forwarding shutdown signals to the canonical workload process - // group after it drops to the sandbox UID requires CAP_KILL. - "KILL".into(), ], // SETUID, SETGID, SETPCAP, CHOWN, and FOWNER are intentionally kept from // Podman's default set and not dropped: @@ -1216,29 +1110,6 @@ pub fn build_container_spec_for_image( mode: 0o400, }); } - if let Some([ca, cert, key]) = tls_secret_names { - secrets.push(SecretMount { - source: ca.clone(), - target: TLS_CA_MOUNT_PATH.into(), - uid: 0, - gid: 0, - mode: 0o400, - }); - secrets.push(SecretMount { - source: cert.clone(), - target: TLS_CERT_MOUNT_PATH.into(), - uid: 0, - gid: 0, - mode: 0o400, - }); - secrets.push(SecretMount { - source: key.clone(), - target: TLS_KEY_MOUNT_PATH.into(), - uid: 0, - gid: 0, - mode: 0o400, - }); - } secrets }, stop_timeout: config.stop_timeout_secs, @@ -1246,11 +1117,6 @@ pub fn build_container_spec_for_image( // reach services on the host. `host.openshell.internal` is the driver- // neutral alias used by policies and e2e tests. hostadd: hostadd_entries(config), - // Preserve Podman's resolver defaults for both policy-DNS and ordinary - // sandboxes. Namespace-local capture supports UDP and TCP, so it must - // not depend on a libc-specific option or alter short-name searches. - dns_search: Vec::new(), - dns_option: Vec::new(), netns: NetNS { nsmode: "bridge".to_string(), }, @@ -1269,20 +1135,20 @@ pub fn build_container_spec_for_image( destination: openshell_core::container_paths::NETNS_MOUNT_ROOT.into(), options: vec!["rw".into(), "nosuid".into(), "nodev".into()], }]; - // Deliver client TLS materials into the container when mTLS is - // enabled. When userns remaps UIDs (auto, no-map), bind-mounted - // host files are unreadable because the container root maps to a - // different host UID. In that case TLS materials are delivered as - // Podman secrets (handled in the `secrets` block above); otherwise - // use bind mounts. - if tls_secret_names.is_none() - && let (Some(ca), Some(cert), Some(key)) = ( - &config.guest_tls_ca, - &config.guest_tls_cert, - &config.guest_tls_key, - ) - { + // Bind-mount client TLS materials into the container when mTLS + // is enabled. The supervisor reads these via OPENSHELL_TLS_CA, + // OPENSHELL_TLS_CERT, and OPENSHELL_TLS_KEY env vars (set in + // build_env above) to establish an mTLS connection back to the + // gateway. + if let (Some(ca), Some(cert), Some(key)) = ( + &config.guest_tls_ca, + &config.guest_tls_cert, + &config.guest_tls_key, + ) { let mut ro = vec!["ro".into(), "rbind".into()]; + // On SELinux-enabled systems (Fedora, RHEL), bind-mounted + // files need the shared relabel option so the container + // process can read them through the SELinux MAC policy. if is_selinux_enabled() { ro.push("z".into()); } @@ -1305,47 +1171,6 @@ pub fn build_container_spec_for_image( options: ro, }); } - // Bind-mount the corporate proxy CA bundle read-only when - // configured. A CA certificate is not secret, so unlike the proxy - // credential (a driver secret) a plain read-only bind mount is - // used. The supervisor reads it via the --upstream-proxy-ca-bundle - // argv path (see upstream_proxy_cli_args) to verify an https:// - // proxy and trust re-signed upstream certificates. - if let Some(ca_bundle) = &config.proxy_ca_bundle { - let mut ro = vec!["ro".into(), "rbind".into()]; - if is_selinux_enabled() { - ro.push("z".into()); - } - m.push(Mount { - kind: "bind".into(), - source: ca_bundle.clone(), - destination: PROXY_CA_MOUNT_PATH.into(), - options: ro, - }); - } - if let Some(bin_path) = supervisor_bin_path { - let mut opts = vec!["ro".into(), "rbind".into()]; - if is_selinux_enabled() { - opts.push("z".into()); - } - m.push(Mount { - kind: "bind".into(), - source: bin_path.display().to_string(), - destination: SUPERVISOR_BINARY_PATH.into(), - options: opts, - }); - } - if let Some(path) = provider_spiffe_workload_api_socket_mount_source(config) { - // No SELinux relabel - the SPIRE agent socket is shared host - // infrastructure and must keep its existing SELinux context. - let ro = vec!["ro".into(), "rbind".into()]; - m.push(Mount { - kind: "bind".into(), - source: path.display().to_string(), - destination: PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR.into(), - options: ro, - }); - } m.extend(user_mounts.mounts); m }, @@ -1357,68 +1182,11 @@ pub fn build_container_spec_for_image( container_port: openshell_core::config::DEFAULT_SSH_PORT, protocol: "tcp".into(), }], - userns: config.userns.as_deref().map(|raw| { - let (base, params) = raw - .split_once(':') - .map_or((raw, None), |(b, p)| (b, Some(p))); - UserNS { - nsmode: base.to_string(), - value: params.map(ToString::to_string), - } - }), - idmappings: match config - .userns - .as_deref() - .map(|m| m.split(':').next().unwrap_or(m)) - { - Some("auto") => Some(IDMappings { - host_uid_mapping: false, - host_gid_mapping: false, - auto_user_ns: true, - uid_map: Vec::new(), - gid_map: Vec::new(), - }), - Some("private") => Some(IDMappings { - host_uid_mapping: false, - host_gid_mapping: false, - auto_user_ns: false, - uid_map: parse_id_maps(&config.uidmap)?, - gid_map: parse_id_maps(&config.gidmap)?, - }), - _ => None, - }, }; Ok(serde_json::to_value(container_spec).expect("ContainerSpec serialization cannot fail")) } -fn provider_spiffe_workload_api_socket_env_value(config: &PodmanComputeConfig) -> Option { - let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; - let raw = host_path.to_str()?; - if raw.starts_with("tcp:") { - return Some(raw.to_string()); - } - let host_path = raw - .strip_prefix("unix:") - .map_or(host_path.as_path(), Path::new); - let file_name = host_path.file_name()?.to_str()?; - Some(format!( - "{PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR}/{file_name}" - )) -} - -fn provider_spiffe_workload_api_socket_mount_source(config: &PodmanComputeConfig) -> Option<&Path> { - let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; - let raw = host_path.to_str()?; - if raw.starts_with("tcp:") { - return None; - } - let host_path = raw - .strip_prefix("unix:") - .map_or(host_path.as_path(), Path::new); - host_path.parent() -} - fn hostadd_entries(config: &PodmanComputeConfig) -> Vec { let host_gateway_ip = config.host_gateway_ip.trim(); if host_gateway_ip.is_empty() { @@ -1616,8 +1384,6 @@ mod tests { "registry.example/app:latest", "sha256:immutable", "app:staff", - None, - None, ) .unwrap(); @@ -1628,8 +1394,6 @@ mod tests { ); assert_eq!(container["user"].as_str(), Some("0:0")); assert_eq!(container["image_pull_policy"].as_str(), Some("never")); - assert_eq!(container["dns_search"], serde_json::json!([])); - assert_eq!(container["dns_option"], serde_json::json!([])); assert_eq!( container["env"][openshell_core::sandbox_env::OCI_IMAGE_USER].as_str(), Some("app:staff") @@ -1648,24 +1412,6 @@ mod tests { ); } - #[test] - fn build_env_strips_gateway_tls_server_name() { - let mut sandbox = test_sandbox("test-id", "test-name"); - let spec = sandbox.spec.get_or_insert_default(); - spec.environment.insert( - openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME.to_string(), - "evil.attacker.example.com".to_string(), - ); - - let container = build_container_spec(&sandbox, &test_config()); - - assert_eq!( - container["env"].get(openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME), - None, - "GATEWAY_TLS_SERVER_NAME must be stripped from the supervisor environment" - ); - } - #[test] fn volume_name_uses_id() { assert_eq!( @@ -1886,7 +1632,6 @@ mod tests { "missing DAC_READ_SEARCH" ); assert!(added.contains(&"SETPCAP"), "missing SETPCAP"); - assert!(added.contains(&"KILL"), "missing KILL"); // SETUID and SETGID are NOT in cap_add — they remain available from the // default bounding set because we no longer use cap_drop:ALL. Verify they @@ -1902,10 +1647,6 @@ mod tests { .collect(); assert!(!dropped.contains(&"SETUID"), "SETUID must not be dropped"); assert!(!dropped.contains(&"SETGID"), "SETGID must not be dropped"); - assert!( - dropped.contains(&"NET_BIND_SERVICE"), - "NET_BIND_SERVICE must stay dropped; policy DNS binds an unprivileged port" - ); assert!( !dropped.contains(&"CHOWN"), "CHOWN must not be dropped (needed for prepare_filesystem chown)" @@ -1918,10 +1659,6 @@ mod tests { !dropped.contains(&"SETPCAP"), "SETPCAP must not be dropped (needed for child bounding-set clear)" ); - assert!( - !dropped.contains(&"KILL"), - "KILL must not be dropped (needed to signal the sandbox workload on shutdown)" - ); assert!( !dropped.contains(&"ALL"), "must not use cap_drop:ALL in rootless Podman" @@ -2070,26 +1807,6 @@ mod tests { ); } - #[test] - fn container_spec_keeps_network_capabilities_driver_controlled() { - use openshell_core::proto::compute::v1::{DriverSandboxSpec, DriverSandboxTemplate}; - - let mut sandbox = test_sandbox("test-id", "legit-name"); - sandbox.spec = Some(DriverSandboxSpec { - environment: std::collections::HashMap::from([( - openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES.to_string(), - "spoofed".to_string(), - )]), - template: Some(DriverSandboxTemplate::default()), - ..Default::default() - }); - let spec = build_container_spec(&sandbox, &test_config()); - assert_eq!( - spec["env"][openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES], - serde_json::json!(openshell_core::sandbox_env::POLICY_DNS_TRANSPARENT_TCP_CAPABILITY) - ); - } - /// Extract the container spec's supervisor argv (`command`) as strings. fn spec_command(spec: &Value) -> Vec { spec["command"] @@ -2157,61 +1874,6 @@ mod tests { ); } - #[test] - fn container_spec_binds_proxy_ca_bundle_and_passes_argv() { - let sandbox = test_sandbox("ca-id", "ca-name"); - let mut config = test_config(); - config.https_proxy = Some("https://proxy.corp.com:3130".to_string()); - config.proxy_ca_bundle = Some("/host/proxy-ca.pem".to_string()); - - let spec = build_container_spec(&sandbox, &config); - let command = spec_command(&spec); - - // The container-side mount path travels on argv as a flag/value pair. - let idx = command - .iter() - .position(|a| a == "--upstream-proxy-ca-bundle") - .expect("CA bundle flag present"); - assert_eq!( - command.get(idx + 1).map(String::as_str), - Some("/etc/openshell/tls/proxy/ca-bundle.pem") - ); - - // The host PEM is bind-mounted read-only at that container path. - let mounts = spec["mounts"] - .as_array() - .expect("mounts should be an array"); - let ca_mount = mounts - .iter() - .find(|m| { - m["type"].as_str() == Some("bind") - && m["destination"].as_str() == Some("/etc/openshell/tls/proxy/ca-bundle.pem") - }) - .expect("CA bundle bind mount present"); - assert_eq!(ca_mount["source"].as_str(), Some("/host/proxy-ca.pem")); - assert!( - ca_mount["options"] - .as_array() - .expect("options array") - .iter() - .any(|o| o.as_str() == Some("ro")), - "CA bundle mount must be read-only" - ); - } - - #[test] - fn container_spec_omits_proxy_ca_bundle_when_unconfigured() { - let sandbox = test_sandbox("test-id", "test-name"); - let spec = build_container_spec(&sandbox, &test_config()); - let mounts = spec["mounts"].as_array().expect("mounts array"); - assert!( - !mounts.iter().any( - |m| m["destination"].as_str() == Some("/etc/openshell/tls/proxy/ca-bundle.pem") - ), - "no CA bundle mount without operator config" - ); - } - #[test] fn container_spec_sandbox_env_cannot_influence_proxy_argv() { use openshell_core::proto::compute::v1::{DriverSandboxSpec, DriverSandboxTemplate}; @@ -3102,33 +2764,6 @@ mod tests { ); } - #[test] - fn container_spec_includes_provider_spiffe_socket_when_configured() { - let sandbox = test_sandbox("spiffe-id", "spiffe-name"); - let mut config = test_config(); - config.provider_spiffe_workload_api_socket = - Some(std::path::PathBuf::from("/host/spire-agent.sock")); - - let spec = build_container_spec(&sandbox, &config); - - let env_map = spec["env"].as_object().expect("env should be an object"); - assert_eq!( - env_map - .get(openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET) - .and_then(|v| v.as_str()), - Some("/spiffe-workload-api/spire-agent.sock"), - ); - - let mounts = spec["mounts"] - .as_array() - .expect("mounts should be an array"); - assert!(mounts.iter().any(|m| { - m["type"].as_str() == Some("bind") - && m["source"].as_str() == Some("/host") - && m["destination"].as_str() == Some(PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR) - })); - } - #[test] fn container_spec_omits_tls_without_config() { let sandbox = test_sandbox("notls-id", "notls-name"); @@ -3151,205 +2786,4 @@ mod tests { .count(); assert_eq!(bind_count, 0, "no bind mounts without TLS config"); } - - #[test] - fn container_spec_includes_userns_when_configured() { - let sandbox = test_sandbox("userns-id", "userns-name"); - let mut config = test_config(); - config.userns = Some("auto".to_string()); - let spec = build_container_spec(&sandbox, &config); - - let userns = &spec["userns"]; - assert_eq!(userns["nsmode"].as_str(), Some("auto")); - assert!(userns.get("value").is_none(), "bare auto should omit value"); - - let idmappings = &spec["idmappings"]; - assert_eq!( - idmappings["AutoUserNs"].as_bool(), - Some(true), - "idmappings.AutoUserNs must be true for userns=auto" - ); - } - - #[test] - fn container_spec_auto_with_params() { - let sandbox = test_sandbox("userns-auto-params-id", "userns-auto-params-name"); - let mut config = test_config(); - config.userns = Some("auto:size=65536".to_string()); - let spec = build_container_spec(&sandbox, &config); - - let userns = &spec["userns"]; - assert_eq!(userns["nsmode"].as_str(), Some("auto")); - assert_eq!(userns["value"].as_str(), Some("size=65536")); - - assert_eq!( - spec["idmappings"]["AutoUserNs"].as_bool(), - Some(true), - "idmappings.AutoUserNs must be true for auto:size=65536" - ); - } - - #[test] - fn container_spec_keep_id_with_params() { - let sandbox = test_sandbox("userns-keepid-id", "userns-keepid-name"); - let mut config = test_config(); - config.userns = Some("keep-id:uid=1000,gid=1000".to_string()); - let spec = build_container_spec(&sandbox, &config); - - let userns = &spec["userns"]; - assert_eq!(userns["nsmode"].as_str(), Some("keep-id")); - assert_eq!(userns["value"].as_str(), Some("uid=1000,gid=1000")); - - assert!( - spec.get("idmappings").is_none(), - "idmappings should not be set for keep-id" - ); - } - - #[test] - fn container_spec_nomap_mode() { - let sandbox = test_sandbox("userns-nomap-id", "userns-nomap-name"); - let mut config = test_config(); - config.userns = Some("no-map".to_string()); - let spec = build_container_spec(&sandbox, &config); - - let userns = &spec["userns"]; - assert_eq!(userns["nsmode"].as_str(), Some("no-map")); - assert!(userns.get("value").is_none(), "no-map should omit value"); - - assert!( - spec.get("idmappings").is_none(), - "idmappings should not be set for no-map" - ); - } - - #[test] - fn container_spec_private_with_mappings() { - let sandbox = test_sandbox("userns-private-id", "userns-private-name"); - let mut config = test_config(); - config.userns = Some("private".to_string()); - config.uidmap = vec!["0:1000:1".to_string(), "1:100000:65536".to_string()]; - config.gidmap = vec!["0:1000:1".to_string(), "1:100000:65536".to_string()]; - let spec = build_container_spec(&sandbox, &config); - - let userns = &spec["userns"]; - assert_eq!(userns["nsmode"].as_str(), Some("private")); - assert!(userns.get("value").is_none(), "private should omit value"); - - let idmappings = &spec["idmappings"]; - assert_eq!( - idmappings["AutoUserNs"].as_bool(), - Some(false), - "AutoUserNs must be false for private mode" - ); - - let uid_map = idmappings["UIDMap"] - .as_array() - .expect("UIDMap should be an array"); - assert_eq!(uid_map.len(), 2); - assert_eq!(uid_map[0]["container_id"].as_u64(), Some(0)); - assert_eq!(uid_map[0]["host_id"].as_u64(), Some(1000)); - assert_eq!(uid_map[0]["size"].as_u64(), Some(1)); - assert_eq!(uid_map[1]["container_id"].as_u64(), Some(1)); - assert_eq!(uid_map[1]["host_id"].as_u64(), Some(100_000)); - assert_eq!(uid_map[1]["size"].as_u64(), Some(65536)); - - let gid_map = idmappings["GIDMap"] - .as_array() - .expect("GIDMap should be an array"); - assert_eq!(gid_map.len(), 2); - assert_eq!(gid_map[0]["container_id"].as_u64(), Some(0)); - assert_eq!(gid_map[0]["host_id"].as_u64(), Some(1000)); - assert_eq!(gid_map[0]["size"].as_u64(), Some(1)); - } - - #[test] - fn container_spec_omits_userns_when_unset() { - let sandbox = test_sandbox("no-userns-id", "no-userns-name"); - let config = test_config(); - let spec = build_container_spec(&sandbox, &config); - - assert!( - spec.get("userns").is_none(), - "userns should not be set when unconfigured" - ); - assert!( - spec.get("idmappings").is_none(), - "idmappings should not be set when userns is unconfigured" - ); - } - - #[test] - fn container_spec_uses_bind_mount_for_supervisor_when_path_provided() { - let sandbox = test_sandbox("bind-sv-id", "bind-sv-name"); - let config = test_config(); - let image = resolve_image(&sandbox, &config); - let spec = build_container_spec_for_image( - &sandbox, - &config, - None, - None, - image, - image, - "", - Some(Path::new("/host/cache/openshell-sandbox")), - None, - ) - .unwrap(); - - let image_volumes = spec["image_volumes"] - .as_array() - .expect("image_volumes should be an array"); - assert!( - !image_volumes - .iter() - .any(|v| v["destination"].as_str() == Some(SUPERVISOR_MOUNT_DIR)), - "supervisor image volume should not be present when bind path is provided" - ); - - let mounts = spec["mounts"] - .as_array() - .expect("mounts should be an array"); - let sv_bind = mounts - .iter() - .find(|m| m["destination"].as_str() == Some(SUPERVISOR_BINARY_PATH)); - assert!( - sv_bind.is_some(), - "supervisor bind mount should be present at {SUPERVISOR_BINARY_PATH}" - ); - let sv_bind = sv_bind.unwrap(); - assert_eq!( - sv_bind["source"].as_str(), - Some("/host/cache/openshell-sandbox") - ); - assert_eq!(sv_bind["type"].as_str(), Some("bind")); - } - - #[test] - fn container_spec_uses_image_volume_when_no_bind_path() { - let sandbox = test_sandbox("imgvol-id", "imgvol-name"); - let config = test_config(); - let spec = build_container_spec(&sandbox, &config); - - let image_volumes = spec["image_volumes"] - .as_array() - .expect("image_volumes should be an array"); - assert!( - image_volumes - .iter() - .any(|v| v["destination"].as_str() == Some(SUPERVISOR_MOUNT_DIR)), - "supervisor image volume should be present by default" - ); - - let mounts = spec["mounts"] - .as_array() - .expect("mounts should be an array"); - assert!( - !mounts.iter().any( - |m| m["destination"].as_str() == Some(SUPERVISOR_BINARY_PATH) - && m["type"].as_str() == Some("bind") - ), - "supervisor bind mount should not be present by default" - ); - } } diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs index 345ea57175..5aaa805934 100644 --- a/crates/openshell-ocsf/src/lib.rs +++ b/crates/openshell-ocsf/src/lib.rs @@ -64,5 +64,6 @@ pub use builders::{ // --- Tracing layers --- pub use tracing_layers::{ - OCSF_TARGET, OcsfJsonlLayer, OcsfShorthandLayer, clone_current_event, emit_ocsf_event, + OCSF_TARGET, OcsfJsonlLayer, OcsfRelayLayer, OcsfRelaySink, OcsfShorthandLayer, + clone_current_event, emit_ocsf_event, }; diff --git a/crates/openshell-ocsf/src/tracing_layers/mod.rs b/crates/openshell-ocsf/src/tracing_layers/mod.rs index c8e5d9f2e4..05cd7e9f80 100644 --- a/crates/openshell-ocsf/src/tracing_layers/mod.rs +++ b/crates/openshell-ocsf/src/tracing_layers/mod.rs @@ -9,8 +9,10 @@ pub(crate) mod event_bridge; mod jsonl_layer; +mod relay_layer; mod shorthand_layer; pub use event_bridge::{OCSF_TARGET, clone_current_event, emit_ocsf_event}; pub use jsonl_layer::OcsfJsonlLayer; +pub use relay_layer::{OcsfRelayLayer, OcsfRelaySink}; pub use shorthand_layer::OcsfShorthandLayer; diff --git a/crates/openshell-ocsf/src/tracing_layers/relay_layer.rs b/crates/openshell-ocsf/src/tracing_layers/relay_layer.rs new file mode 100644 index 0000000000..a4949924d5 --- /dev/null +++ b/crates/openshell-ocsf/src/tracing_layers/relay_layer.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tracing layer that captures OCSF events and forwards them as JSON bytes +//! through a telemetry buffer sender for relay to the gateway. + +use std::sync::Arc; + +use tracing::Subscriber; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; + +use super::event_bridge::{OCSF_TARGET, clone_current_event}; + +/// Callback trait for delivering serialized OCSF events. +pub trait OcsfRelaySink: Send + Sync + 'static { + fn send(&self, json_bytes: Vec); +} + +/// A tracing layer that captures OCSF events and serializes them to JSON +/// for relay through the telemetry transport. +pub struct OcsfRelayLayer { + sink: Arc, +} + +impl OcsfRelayLayer { + pub fn new(sink: Arc) -> Self { + Self { sink } + } +} + +impl Layer for OcsfRelayLayer { + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + if event.metadata().target() != OCSF_TARGET { + return; + } + + let Some(ocsf_event) = clone_current_event() else { + return; + }; + + if let Ok(json) = serde_json::to_vec(&ocsf_event) { + self.sink.send(json); + } + } +} diff --git a/crates/openshell-otel/src/lib.rs b/crates/openshell-otel/src/lib.rs index 7a9162ab92..819265fb1e 100644 --- a/crates/openshell-otel/src/lib.rs +++ b/crates/openshell-otel/src/lib.rs @@ -5,7 +5,7 @@ mod driver; mod grpc; -mod propagation; +pub mod propagation; pub use driver::{ BoxGrpcStream, ComputeDriverTracing, DriverTracingConfig, DriverTracingHandle, diff --git a/crates/openshell-otel/src/propagation.rs b/crates/openshell-otel/src/propagation.rs index c6e51a0a6b..6a9b1498c9 100644 --- a/crates/openshell-otel/src/propagation.rs +++ b/crates/openshell-otel/src/propagation.rs @@ -54,6 +54,22 @@ impl Injector for MetadataMapInjector<'_> { } } +/// Writes OpenTelemetry propagation fields to HTTP headers. +#[derive(Debug)] +pub struct HeaderMapInjector<'a>(pub &'a mut HeaderMap); + +impl Injector for HeaderMapInjector<'_> { + fn set(&mut self, key: &str, value: String) { + let Ok(key) = key.parse::() else { + return; + }; + let Ok(value) = value.parse() else { + return; + }; + self.0.insert(key, value); + } +} + #[derive(Debug)] struct TraceContextMapInjector<'a>(&'a mut BTreeMap); @@ -75,6 +91,15 @@ pub fn current_trace_context_carrier() -> Option> { carrier.contains_key("traceparent").then_some(carrier) } +/// Inject W3C traceparent into HTTP headers if not already present. +pub fn inject_traceparent_if_missing(headers: &mut HeaderMap) { + if headers.contains_key("traceparent") { + return; + } + let context = tracing::Span::current().context(); + TraceContextPropagator::new().inject_context(&context, &mut HeaderMapInjector(headers)); +} + /// Injects the active W3C trace context into an outbound tonic request. #[derive(Debug, Clone, Copy)] pub struct TraceContextInterceptor; diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 7afae200b5..20948fb159 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -852,6 +852,9 @@ pub async fn run_sandbox( }; tokio::pin!(proxy_exited); + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] + let mut telemetry_relay_handle: Option = None; + let exit_code = if process_enabled { let ca_file_paths = networking .as_ref() @@ -952,6 +955,66 @@ pub async fn run_sandbox( None }; + // Telemetry relay: bind OTLP receiver on 127.0.0.1:4318 for all + // Linux topologies. For Docker/Podman with a netns, bind inside the + // namespace. For K8s/VM (shared namespace), bind directly. + let telemetry_rx = { + #[cfg(target_os = "linux")] + { + let (telemetry_session_tx, telemetry_session_rx) = + tokio::sync::mpsc::channel::(64); + + let relay_config = openshell_supervisor_network::otlp::RelayConfig::default(); + let metadata = openshell_supervisor_network::otlp::SandboxMetadata { + sandbox_id: sandbox_id.clone().unwrap_or_default(), + workspace_id: workspace_rx.borrow().clone(), + policy: sandbox_name_for_agg.clone().unwrap_or_default(), + user: resolved_process_identity + .uid() + .map_or_else(String::new, |uid| uid.to_string()), + image: std::env::var("OPENSHELL_CONTAINER_IMAGE").unwrap_or_default(), + driver: std::env::var(openshell_core::sandbox_env::SUPERVISOR_TOPOLOGY) + .unwrap_or_else(|_| "container".to_string()), + }; + let relay = openshell_supervisor_network::otlp::TelemetryRelay::new( + relay_config, + metadata, + telemetry_session_tx, + ); + + let localhost_addr: std::net::SocketAddr = "127.0.0.1:4318".parse().unwrap(); + + if let Some(ns) = netns.as_ref() { + match ns.bind_tcp_in_netns("127.0.0.1:4318").await { + Ok(listener) => { + let handle = relay.start_with_listener(listener); + tracing::info!(bind = %localhost_addr, "telemetry relay started (netns)"); + telemetry_relay_handle = Some(handle); + } + Err(e) => { + tracing::warn!(error = %e, "telemetry relay failed to bind in netns; continuing without relay"); + } + } + } else { + match relay.start(localhost_addr).await { + Ok(handle) => { + tracing::info!(bind = %localhost_addr, "telemetry relay started"); + telemetry_relay_handle = Some(handle); + } + Err(e) => { + tracing::warn!(error = %e, "telemetry relay failed to start; continuing without relay"); + } + } + } + Some(telemetry_session_rx) + } + #[cfg(not(target_os = "linux"))] + { + debug!("telemetry relay not available on this platform"); + None + } + }; + let process = openshell_supervisor_process::run::run_process( program, args, @@ -980,6 +1043,7 @@ pub async fn run_sandbox( bypass_denial_tx, #[cfg(target_os = "linux")] bypass_activity_tx, + telemetry_rx, ); if let Some(control_closed) = process_control_closed.as_mut() { @@ -1139,6 +1203,12 @@ pub async fn run_sandbox( } }; + // Drain telemetry relay before tearing down networking so short-lived + // agents don't lose their final spans. + if let Some(handle) = telemetry_relay_handle { + handle.shutdown().await; + } + // Drop networking explicitly so the proxy + bypass monitor RAII // handles tear down before we return. drop(networking); @@ -1456,6 +1526,7 @@ fn spawn_sidecar_entrypoint_handler( Some(supervisor_pid), Arc::clone(&terminating), started.instance_id.clone(), + None, )); session_started = true; info!("sidecar supervisor session task spawned"); diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 2619fee5cc..2a3d4fe237 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -73,6 +73,7 @@ tracing-subscriber = { workspace = true } # OpenTelemetry (OTLP trace export, opt-in via [openshell.gateway.otlp]) opentelemetry = { workspace = true } opentelemetry_sdk = { workspace = true } +opentelemetry-proto = { workspace = true } tracing-opentelemetry = { workspace = true } # Metrics diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index a8c8afdf08..881351b83b 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -38,6 +38,7 @@ mod service_routing; mod ssh_sessions; pub mod supervisor_session; mod telemetry; +pub(crate) mod telemetry_relay; #[cfg(any(test, feature = "test-support"))] pub mod test_support; mod tls; @@ -341,6 +342,10 @@ pub struct ServerState { /// Empty when OIDC is not configured — `authorize_workspace()` treats /// every authenticated user as Platform Admin in that case. pub admin_role: String, + + /// Dedicated OTLP exporter for relayed telemetry from supervisors. + /// `None` when the gateway has no OTLP endpoint configured. + pub telemetry_relay_exporter: Option>, } fn is_benign_tls_handshake_failure(error: &std::io::Error) -> bool { @@ -428,6 +433,7 @@ impl ServerState { provider_profile_sources: provider_profile_sources::ProviderProfileSources::with_default_sources(), admin_role, + telemetry_relay_exporter: None, } } } @@ -661,6 +667,8 @@ pub(crate) async fn run_server( state.middleware_registry = middleware_registry; state.gateway_interceptors = gateway_interceptors; state.provider_profile_sources = provider_profile_sources; + state.telemetry_relay_exporter = + telemetry_relay::try_create_exporter(config_file.as_ref()).await; state.sandbox_jwt_issuer = sandbox_jwt_issuer.clone(); state.sandbox_jwt_authenticator = sandbox_jwt_authenticator; if let Some(issuer) = sandbox_jwt_issuer { diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index c8491dc1eb..ec320dc5ea 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -769,11 +769,15 @@ pub async fn handle_connect_supervisor( ); } - // Step 3: Send SessionAccepted. + // Step 3: Determine confirmed capabilities. + let confirmed_capabilities = confirm_capabilities(&hello.capabilities, state); + + // Step 4: Send SessionAccepted. let accepted = GatewayMessage { payload: Some(gateway_message::Payload::SessionAccepted(SessionAccepted { session_id: session_id.clone(), heartbeat_interval_secs: HEARTBEAT_INTERVAL_SECS, + capabilities: confirmed_capabilities, })), }; if tx.send(accepted).await.is_err() { @@ -1016,11 +1020,66 @@ fn handle_supervisor_message( "supervisor session: relay closed by supervisor" ); } + Some(supervisor_message::Payload::Telemetry(telemetry)) => { + handle_telemetry_data(state, sandbox_id, session_id, telemetry); + } _ => { - warn!( + debug!( + sandbox_id = %sandbox_id, + session_id = %session_id, + "supervisor session: unknown message type (future extension)" + ); + } + } +} + +/// Check which advertised capabilities the gateway can confirm. +fn confirm_capabilities(advertised: &[String], state: &Arc) -> Vec { + let mut confirmed = Vec::new(); + for cap in advertised { + match cap.as_str() { + "telemetry_relay" if state.telemetry_relay_exporter.is_some() => { + confirmed.push(cap.clone()); + } + _ => {} + } + } + confirmed +} + +/// Handle incoming telemetry data from the supervisor: forward trace data to +/// the configured OTLP collector and dispatch OCSF events to the log sink. +fn handle_telemetry_data( + state: &Arc, + sandbox_id: &str, + session_id: &str, + telemetry: openshell_core::proto::TelemetryData, +) { + if !telemetry.trace_data.is_empty() + && let Some(relay_exporter) = state.telemetry_relay_exporter.as_ref() + { + let exporter = relay_exporter.clone(); + let trace_data = telemetry.trace_data; + let sandbox_id = sandbox_id.to_string(); + tokio::spawn(async move { + if let Err(e) = exporter.export_raw(trace_data).await { + debug!( + sandbox_id = %sandbox_id, + error = %e, + "telemetry relay: failed to export trace data" + ); + } + }); + } + + for ocsf_event in &telemetry.ocsf_events { + if let Ok(json_str) = std::str::from_utf8(ocsf_event) { + info!( + target: "ocsf_relay", sandbox_id = %sandbox_id, session_id = %session_id, - "supervisor session: unexpected message type" + "{}", + json_str ); } } diff --git a/crates/openshell-server/src/telemetry_relay.rs b/crates/openshell-server/src/telemetry_relay.rs new file mode 100644 index 0000000000..57abe75de5 --- /dev/null +++ b/crates/openshell-server/src/telemetry_relay.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Dedicated OTLP exporter for relayed telemetry from supervisors. +//! +//! Uses a separate gRPC client to forward pre-enriched trace data to the +//! configured OTLP collector, bypassing the gateway's own `SdkTracerProvider` +//! which would overwrite resource attributes. + +use std::sync::Arc; + +use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; +use prost::Message; +use tonic::transport::Channel; +use tracing::debug; + +use opentelemetry_proto::tonic::collector::trace::v1::trace_service_client::TraceServiceClient; + +/// Exporter that forwards raw protobuf-encoded trace data to an OTLP collector. +#[derive(Debug, Clone)] +pub struct TelemetryRelayExporter { + client: TraceServiceClient, +} + +impl TelemetryRelayExporter { + /// Connect to the OTLP collector at the given gRPC endpoint. + pub async fn connect(endpoint: &str) -> Result { + let channel = Channel::from_shared(endpoint.to_string()) + .expect("valid OTLP endpoint URL") + .connect() + .await?; + Ok(Self { + client: TraceServiceClient::new(channel), + }) + } + + /// Export raw protobuf-encoded `ExportTraceServiceRequest` bytes. + pub async fn export_raw(&self, trace_data: Vec) -> Result<(), ExportError> { + let request = ExportTraceServiceRequest::decode(trace_data.as_slice()) + .map_err(ExportError::Decode)?; + + let mut client = self.client.clone(); + client + .export(tonic::Request::new(request)) + .await + .map_err(ExportError::Grpc)?; + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ExportError { + #[error("failed to decode trace data: {0}")] + Decode(prost::DecodeError), + #[error("gRPC export failed: {0}")] + Grpc(tonic::Status), +} + +/// Create a relay exporter from the gateway's OTLP config, if configured. +pub async fn try_create_exporter( + config_file: Option<&crate::config_file::ConfigFile>, +) -> Option> { + let otlp = config_file?.openshell.gateway.otlp.as_ref()?; + match TelemetryRelayExporter::connect(&otlp.endpoint).await { + Ok(exporter) => { + debug!(endpoint = %otlp.endpoint, "telemetry relay exporter connected"); + Some(Arc::new(exporter)) + } + Err(e) => { + tracing::warn!( + endpoint = %otlp.endpoint, + error = %e, + "failed to connect telemetry relay exporter; relay disabled" + ); + None + } + } +} diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index 34d9c32a47..94c34b5a2d 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -13,10 +13,16 @@ rust-version.workspace = true [dependencies] openshell-core = { path = "../openshell-core", features = ["oauth"] } openshell-ocsf = { path = "../openshell-ocsf" } +openshell-otel = { path = "../openshell-otel" } openshell-policy = { path = "../openshell-policy" } openshell-router = { path = "../openshell-router" } openshell-supervisor-middleware = { path = "../openshell-supervisor-middleware" } +hyper = { workspace = true } +hyper-util = { version = "0.1", features = ["tokio", "http1"] } +http-body-util = "0.1" +opentelemetry-proto = { workspace = true } +prost = { workspace = true } apollo-parser = { workspace = true } aws-sigv4 = { version = "1", features = ["sign-http", "http1"] } aws-credential-types = { version = "1", features = ["hardcoded-credentials"] } diff --git a/crates/openshell-supervisor-network/src/lib.rs b/crates/openshell-supervisor-network/src/lib.rs index 4fec48b300..88f9964e37 100644 --- a/crates/openshell-supervisor-network/src/lib.rs +++ b/crates/openshell-supervisor-network/src/lib.rs @@ -12,6 +12,7 @@ pub mod identity; pub mod inference_routes; pub mod l7; pub mod opa; +pub mod otlp; pub(crate) mod policy_dns; pub mod policy_local; pub mod procfs; diff --git a/crates/openshell-supervisor-network/src/otlp/buffer.rs b/crates/openshell-supervisor-network/src/otlp/buffer.rs new file mode 100644 index 0000000000..acd97c2da9 --- /dev/null +++ b/crates/openshell-supervisor-network/src/otlp/buffer.rs @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Bounded telemetry buffer with ring-buffer drop semantics. + +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + +use tokio::sync::mpsc; + +/// Distinguishes trace data from OCSF events in the shared buffer. +#[derive(Debug)] +pub enum TelemetryItem { + Trace(Vec), + Ocsf(Vec), +} + +/// Shared drop/depth counters for the buffer. +#[derive(Debug, Clone)] +pub struct BufferMetrics { + drop_count: Arc, + queue_depth: Arc, +} + +impl BufferMetrics { + pub fn drops(&self) -> u64 { + self.drop_count.load(Ordering::Relaxed) + } + + pub fn depth(&self) -> usize { + self.queue_depth.load(Ordering::Relaxed) + } +} + +/// Sender half of the telemetry buffer. Implements ring-buffer drop semantics: +/// when the buffer is full, the send still succeeds but the oldest entry is +/// lost and the drop counter increments. +#[derive(Clone)] +pub struct TelemetrySender { + tx: mpsc::Sender, + metrics: BufferMetrics, +} + +impl TelemetrySender { + /// Send a telemetry item into the buffer. If the channel is full, the + /// item is dropped and the drop counter is incremented. + pub fn send(&self, item: TelemetryItem) { + match self.tx.try_send(item) { + Ok(()) => { + self.metrics.queue_depth.fetch_add(1, Ordering::Relaxed); + } + Err(_) => { + self.metrics.drop_count.fetch_add(1, Ordering::Relaxed); + } + } + } + + pub fn send_trace(&self, data: Vec) { + self.send(TelemetryItem::Trace(data)); + } + + pub fn send_ocsf(&self, data: Vec) { + self.send(TelemetryItem::Ocsf(data)); + } + + pub fn metrics(&self) -> &BufferMetrics { + &self.metrics + } +} + +/// Receiver half of the telemetry buffer. +pub struct TelemetryReceiver { + rx: mpsc::Receiver, + metrics: BufferMetrics, +} + +impl TelemetryReceiver { + /// Receive the next buffered entry, or `None` if all senders are dropped. + pub async fn recv(&mut self) -> Option { + let item = self.rx.recv().await; + if item.is_some() { + self.metrics.queue_depth.fetch_sub(1, Ordering::Relaxed); + } + item + } + + /// Drain all currently buffered entries without waiting. + pub fn drain(&mut self) -> Vec { + let mut items = Vec::new(); + while let Ok(item) = self.rx.try_recv() { + self.metrics.queue_depth.fetch_sub(1, Ordering::Relaxed); + items.push(item); + } + items + } + + pub fn metrics(&self) -> &BufferMetrics { + &self.metrics + } +} + +/// Create a new telemetry buffer pair with the given capacity. +pub fn new_telemetry_buffer(capacity: usize) -> (TelemetrySender, TelemetryReceiver) { + let (tx, rx) = mpsc::channel(capacity); + let metrics = BufferMetrics { + drop_count: Arc::new(AtomicU64::new(0)), + queue_depth: Arc::new(AtomicUsize::new(0)), + }; + ( + TelemetrySender { + tx, + metrics: metrics.clone(), + }, + TelemetryReceiver { rx, metrics }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn buffer_drops_when_full() { + let (tx, mut rx) = new_telemetry_buffer(2); + + tx.send_trace(vec![1]); + tx.send_trace(vec![2]); + tx.send_trace(vec![3]); + + assert_eq!(tx.metrics().drops(), 1); + assert_eq!(tx.metrics().depth(), 2); + + let first = rx.recv().await.unwrap(); + assert!(matches!(first, TelemetryItem::Trace(v) if v == vec![1])); + assert_eq!(rx.metrics().depth(), 1); + } + + #[tokio::test] + async fn drain_empties_buffer() { + let (tx, mut rx) = new_telemetry_buffer(16); + + tx.send_trace(vec![1]); + tx.send_ocsf(vec![2]); + tx.send_trace(vec![3]); + + let items = rx.drain(); + assert_eq!(items.len(), 3); + assert!(matches!(&items[0], TelemetryItem::Trace(_))); + assert!(matches!(&items[1], TelemetryItem::Ocsf(_))); + assert_eq!(rx.metrics().depth(), 0); + } +} diff --git a/crates/openshell-supervisor-network/src/otlp/enrichment.rs b/crates/openshell-supervisor-network/src/otlp/enrichment.rs new file mode 100644 index 0000000000..d369583c7e --- /dev/null +++ b/crates/openshell-supervisor-network/src/otlp/enrichment.rs @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Span enrichment: injects sandbox resource attributes into OTLP trace data. + +use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; +use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue}; +use opentelemetry_proto::tonic::resource::v1::Resource; +use prost::Message; + +use super::SandboxMetadata; + +/// Content type of the incoming OTLP request body. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContentType { + Protobuf, + Json, +} + +/// Enrich spans with sandbox resource attributes. Input can be protobuf or +/// JSON-encoded `ExportTraceServiceRequest`. Output is always protobuf. +/// +/// When `enrichment_enabled` is false, sandbox metadata attributes are skipped +/// but `openshell.telemetry.source: "agent"` is always injected (relay marker). +pub fn enrich_spans( + raw: &[u8], + content_type: ContentType, + attrs: &SandboxMetadata, + enrichment_enabled: bool, +) -> Result, EnrichmentError> { + let mut request = match content_type { + ContentType::Protobuf => { + ExportTraceServiceRequest::decode(raw).map_err(EnrichmentError::ProtobufDecode)? + } + ContentType::Json => serde_json::from_slice::(raw) + .map_err(EnrichmentError::JsonDecode)?, + }; + + let extra_attrs = build_attributes(attrs, enrichment_enabled); + + for resource_spans in &mut request.resource_spans { + let resource = resource_spans + .resource + .get_or_insert_with(Resource::default); + + for attr in &extra_attrs { + resource.attributes.push(attr.clone()); + } + } + + Ok(request.encode_to_vec()) +} + +fn build_attributes(meta: &SandboxMetadata, enrichment_enabled: bool) -> Vec { + let mut attrs = Vec::new(); + + // Always inject the relay routing marker regardless of enrichment toggle + attrs.push(kv("openshell.telemetry.source", "agent")); + + if enrichment_enabled { + attrs.push(kv("openshell.sandbox.id", &meta.sandbox_id)); + attrs.push(kv("openshell.workspace.id", &meta.workspace_id)); + attrs.push(kv("openshell.sandbox.policy", &meta.policy)); + attrs.push(kv("openshell.sandbox.user", &meta.user)); + attrs.push(kv("openshell.sandbox.image", &meta.image)); + attrs.push(kv("openshell.sandbox.driver", &meta.driver)); + } + + attrs +} + +fn kv(key: &str, value: &str) -> KeyValue { + KeyValue { + key: key.to_string(), + value: Some(AnyValue { + value: Some( + opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue( + value.to_string(), + ), + ), + }), + key_strindex: 0, + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EnrichmentError { + #[error("protobuf decode failed: {0}")] + ProtobufDecode(prost::DecodeError), + #[error("JSON decode failed: {0}")] + JsonDecode(serde_json::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; + use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span}; + use prost::Message; + + fn test_metadata() -> SandboxMetadata { + SandboxMetadata { + sandbox_id: "sb-123".into(), + workspace_id: "ws-456".into(), + policy: "default".into(), + user: "test-user".into(), + image: "test-image:latest".into(), + driver: "docker".into(), + } + } + + fn make_trace_request() -> Vec { + let req = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: None, + scope_spans: vec![ScopeSpans { + scope: None, + spans: vec![Span { + name: "test-span".into(), + ..Default::default() + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + req.encode_to_vec() + } + + #[test] + fn enrichment_adds_all_attributes() { + let raw = make_trace_request(); + let result = enrich_spans(&raw, ContentType::Protobuf, &test_metadata(), true).unwrap(); + + let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); + let resource = decoded.resource_spans[0].resource.as_ref().unwrap(); + + let attr_keys: Vec<&str> = resource.attributes.iter().map(|a| a.key.as_str()).collect(); + assert!(attr_keys.contains(&"openshell.sandbox.id")); + assert!(attr_keys.contains(&"openshell.workspace.id")); + assert!(attr_keys.contains(&"openshell.telemetry.source")); + } + + #[test] + fn enrichment_disabled_only_adds_source() { + let raw = make_trace_request(); + let result = enrich_spans(&raw, ContentType::Protobuf, &test_metadata(), false).unwrap(); + + let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); + let resource = decoded.resource_spans[0].resource.as_ref().unwrap(); + + assert_eq!(resource.attributes.len(), 1); + assert_eq!(resource.attributes[0].key, "openshell.telemetry.source"); + } +} diff --git a/crates/openshell-supervisor-network/src/otlp/mod.rs b/crates/openshell-supervisor-network/src/otlp/mod.rs new file mode 100644 index 0000000000..1bd08743eb --- /dev/null +++ b/crates/openshell-supervisor-network/src/otlp/mod.rs @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OTLP telemetry relay for the sandbox supervisor. +//! +//! Receives OTLP trace data from agent processes over HTTP, enriches spans +//! with sandbox resource attributes, buffers them in a bounded channel, and +//! forwards them to the gateway over the session protocol. + +pub mod buffer; +pub mod enrichment; +pub mod receiver; + +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use tokio::sync::mpsc; +use tracing::info; + +use openshell_core::proto::SupervisorMessage; +use openshell_core::proto::TelemetryData; +use openshell_core::proto::supervisor_message; + +use buffer::{TelemetryReceiver, TelemetrySender}; + +/// Rate-limited OCSF relay sink that implements token bucket rate limiting +/// and sends accepted events through the telemetry buffer as OCSF bytes. +pub struct RateLimitedOcsfSink { + buf_tx: TelemetrySender, + tokens: std::sync::atomic::AtomicU32, + max_tokens: u32, + drop_count: AtomicU64, + last_refill: std::sync::Mutex, +} + +impl RateLimitedOcsfSink { + pub fn new(buf_tx: TelemetrySender, rate_per_sec: u32) -> Self { + Self { + buf_tx, + tokens: std::sync::atomic::AtomicU32::new(rate_per_sec), + max_tokens: rate_per_sec, + drop_count: AtomicU64::new(0), + last_refill: std::sync::Mutex::new(std::time::Instant::now()), + } + } + + fn try_acquire(&self) -> bool { + self.refill(); + let mut current = self.tokens.load(Ordering::Relaxed); + loop { + if current == 0 { + return false; + } + match self.tokens.compare_exchange_weak( + current, + current - 1, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => return true, + Err(updated) => current = updated, + } + } + } + + fn refill(&self) { + let Ok(mut last) = self.last_refill.lock() else { + return; + }; + let now = std::time::Instant::now(); + let elapsed = now.duration_since(*last); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let new_tokens = (elapsed.as_secs_f64() * f64::from(self.max_tokens)) as u32; + if new_tokens > 0 { + *last = now; + let current = self.tokens.load(Ordering::Relaxed); + let capped = (current + new_tokens).min(self.max_tokens); + self.tokens.store(capped, Ordering::Relaxed); + } + } + + pub fn drops(&self) -> u64 { + self.drop_count.load(Ordering::Relaxed) + } +} + +impl openshell_ocsf::OcsfRelaySink for RateLimitedOcsfSink { + fn send(&self, json_bytes: Vec) { + if self.try_acquire() { + self.buf_tx.send_ocsf(json_bytes); + } else { + self.drop_count.fetch_add(1, Ordering::Relaxed); + } + } +} + +/// Configuration for the telemetry relay. +#[derive(Debug, Clone)] +pub struct RelayConfig { + pub enabled: bool, + pub buffer_capacity: usize, + pub enrichment_enabled: bool, + pub ocsf_rate_limit: u32, +} + +impl Default for RelayConfig { + fn default() -> Self { + Self { + enabled: true, + buffer_capacity: 4096, + enrichment_enabled: true, + ocsf_rate_limit: 100, + } + } +} + +/// Sandbox identity used for span enrichment. +#[derive(Debug, Clone)] +pub struct SandboxMetadata { + pub sandbox_id: String, + pub workspace_id: String, + pub policy: String, + pub user: String, + pub image: String, + pub driver: String, +} + +/// Handle returned by [`TelemetryRelay::start`] for lifecycle management. +pub struct RelayHandle { + shutdown_tx: tokio::sync::oneshot::Sender<()>, + forwarder_handle: tokio::task::JoinHandle<()>, + receiver_handle: tokio::task::JoinHandle<()>, + pub telemetry_tx: TelemetrySender, +} + +impl RelayHandle { + /// Gracefully shut down the relay: stop the HTTP receiver, then drain + /// remaining buffered telemetry through the forwarder. + pub async fn shutdown(self) { + let metrics = self.telemetry_tx.metrics().clone(); + let _ = self.shutdown_tx.send(()); + let _ = self.receiver_handle.await; + let _ = self.forwarder_handle.await; + info!( + spans_dropped = metrics.drops(), + queue_depth = metrics.depth(), + "telemetry relay shut down" + ); + } +} + +/// The telemetry relay manages receive, enrich, buffer, and forward. +pub struct TelemetryRelay { + config: RelayConfig, + metadata: SandboxMetadata, + session_tx: mpsc::Sender, + sandbox_id: String, +} + +impl TelemetryRelay { + pub fn new( + config: RelayConfig, + metadata: SandboxMetadata, + session_tx: mpsc::Sender, + ) -> Self { + let sandbox_id = metadata.sandbox_id.clone(); + Self { + config, + metadata, + session_tx, + sandbox_id, + } + } + + /// Start the relay with a pre-bound listener (for netns topologies). + pub fn start_with_listener(self, listener: tokio::net::TcpListener) -> RelayHandle { + let (buf_tx, buf_rx) = buffer::new_telemetry_buffer(self.config.buffer_capacity); + let session_drop_counter = Arc::new(AtomicU64::new(0)); + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + + let receiver_handle = receiver::spawn_receiver_with_listener( + listener, + buf_tx.clone(), + self.metadata.clone(), + self.config.enrichment_enabled, + shutdown_rx, + ); + + let forwarder_handle = spawn_forwarder( + buf_rx, + self.session_tx, + self.sandbox_id.clone(), + session_drop_counter, + ); + + info!( + buffer_capacity = self.config.buffer_capacity, + enrichment = self.config.enrichment_enabled, + "telemetry relay started (pre-bound listener)" + ); + + RelayHandle { + shutdown_tx, + forwarder_handle, + receiver_handle, + telemetry_tx: buf_tx, + } + } + + /// Start the relay: bind the OTLP HTTP receiver and spawn the forwarder. + pub async fn start(self, bind_addr: SocketAddr) -> Result { + let (buf_tx, buf_rx) = buffer::new_telemetry_buffer(self.config.buffer_capacity); + + let session_drop_counter = Arc::new(AtomicU64::new(0)); + + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + + let receiver_handle = receiver::spawn_receiver( + bind_addr, + buf_tx.clone(), + self.metadata.clone(), + self.config.enrichment_enabled, + shutdown_rx, + ) + .await + .map_err(StartError::Bind)?; + + let forwarder_handle = spawn_forwarder( + buf_rx, + self.session_tx, + self.sandbox_id.clone(), + session_drop_counter, + ); + + info!( + bind = %bind_addr, + buffer_capacity = self.config.buffer_capacity, + enrichment = self.config.enrichment_enabled, + "telemetry relay started" + ); + + Ok(RelayHandle { + shutdown_tx, + forwarder_handle, + receiver_handle, + telemetry_tx: buf_tx, + }) + } +} + +/// Errors that can occur when starting the relay. +#[derive(Debug, thiserror::Error)] +pub enum StartError { + #[error("failed to bind OTLP receiver: {0}")] + Bind(std::io::Error), +} + +/// Spawn the forwarder task that drains the buffer and sends `TelemetryData` +/// via the session channel using `try_send` (non-blocking). +fn spawn_forwarder( + mut buf_rx: TelemetryReceiver, + session_tx: mpsc::Sender, + sandbox_id: String, + session_drop_counter: Arc, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + while let Some(item) = buf_rx.recv().await { + let msg = match item { + buffer::TelemetryItem::Trace(data) => SupervisorMessage { + payload: Some(supervisor_message::Payload::Telemetry(TelemetryData { + sandbox_id: sandbox_id.clone(), + trace_data: data, + ocsf_events: Vec::new(), + })), + }, + buffer::TelemetryItem::Ocsf(data) => SupervisorMessage { + payload: Some(supervisor_message::Payload::Telemetry(TelemetryData { + sandbox_id: sandbox_id.clone(), + trace_data: Vec::new(), + ocsf_events: vec![data], + })), + }, + }; + + if session_tx.try_send(msg).is_err() { + session_drop_counter.fetch_add(1, Ordering::Relaxed); + } + } + }) +} diff --git a/crates/openshell-supervisor-network/src/otlp/receiver.rs b/crates/openshell-supervisor-network/src/otlp/receiver.rs new file mode 100644 index 0000000000..69e3ee1bd8 --- /dev/null +++ b/crates/openshell-supervisor-network/src/otlp/receiver.rs @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OTLP HTTP receiver: accepts `POST /v1/traces` with protobuf or JSON. + +use std::convert::Infallible; +use std::net::SocketAddr; + +use bytes::Bytes; +use http_body_util::Full; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Method, Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tracing::{debug, warn}; + +use super::SandboxMetadata; +use super::buffer::TelemetrySender; +use super::enrichment::{self, ContentType, EnrichmentError}; + +/// Spawn the OTLP HTTP receiver using a pre-bound listener. +/// +/// Used for netns topologies where the listener is bound inside the namespace. +pub fn spawn_receiver_with_listener( + listener: TcpListener, + buf_tx: TelemetrySender, + metadata: SandboxMetadata, + enrichment_enabled: bool, + shutdown_rx: oneshot::Receiver<()>, +) -> tokio::task::JoinHandle<()> { + spawn_receiver_inner(listener, buf_tx, metadata, enrichment_enabled, shutdown_rx) +} + +/// Spawn the OTLP HTTP receiver by binding to the given address directly. +pub async fn spawn_receiver( + bind_addr: SocketAddr, + buf_tx: TelemetrySender, + metadata: SandboxMetadata, + enrichment_enabled: bool, + shutdown_rx: oneshot::Receiver<()>, +) -> Result, std::io::Error> { + let listener = TcpListener::bind(bind_addr).await?; + Ok(spawn_receiver_inner( + listener, + buf_tx, + metadata, + enrichment_enabled, + shutdown_rx, + )) +} + +fn spawn_receiver_inner( + listener: TcpListener, + buf_tx: TelemetrySender, + metadata: SandboxMetadata, + enrichment_enabled: bool, + shutdown_rx: oneshot::Receiver<()>, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + let mut shutdown_rx = shutdown_rx; + + loop { + tokio::select! { + accept = listener.accept() => { + match accept { + Ok((stream, _peer)) => { + openshell_core::net::set_tcp_nodelay_best_effort(&stream); + let buf_tx = buf_tx.clone(); + let metadata = metadata.clone(); + tokio::spawn(async move { + let svc = service_fn(move |req| { + let buf_tx = buf_tx.clone(); + let metadata = metadata.clone(); + async move { + handle_request(req, &buf_tx, &metadata, enrichment_enabled).await + } + }); + if let Err(e) = http1::Builder::new() + .serve_connection(TokioIo::new(stream), svc) + .await + { + debug!(error = %e, "OTLP HTTP connection error"); + } + }); + } + Err(e) => { + warn!(error = %e, "OTLP receiver accept error"); + } + } + } + _ = &mut shutdown_rx => { + debug!("OTLP receiver shutting down"); + break; + } + } + } + }) +} + +async fn handle_request( + req: Request, + buf_tx: &TelemetrySender, + metadata: &SandboxMetadata, + enrichment_enabled: bool, +) -> Result>, Infallible> { + if req.method() != Method::POST || req.uri().path() != "/v1/traces" { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::from("{\"error\":\"not found\"}"))) + .unwrap()); + } + + let Some(content_type) = parse_content_type(req.headers()) else { + return Ok(Response::builder() + .status(StatusCode::UNSUPPORTED_MEDIA_TYPE) + .body(Full::new(Bytes::from( + "{\"error\":\"unsupported content type\"}", + ))) + .unwrap()); + }; + + let body = match http_body_util::BodyExt::collect(req.into_body()).await { + Ok(collected) => collected.to_bytes(), + Err(e) => { + warn!(error = %e, "failed to read OTLP request body"); + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::new(Bytes::from( + "{\"error\":\"failed to read body\"}", + ))) + .unwrap()); + } + }; + + match enrichment::enrich_spans(&body, content_type, metadata, enrichment_enabled) { + Ok(enriched) => { + buf_tx.send_trace(enriched); + Ok(Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/x-protobuf") + .body(Full::new(Bytes::new())) + .unwrap()) + } + Err(EnrichmentError::ProtobufDecode(e)) => { + warn!(error = %e, "malformed protobuf OTLP request"); + Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::new(Bytes::from( + "{\"error\":\"malformed protobuf request\"}", + ))) + .unwrap()) + } + Err(EnrichmentError::JsonDecode(e)) => { + warn!(error = %e, "malformed JSON OTLP request"); + Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::new(Bytes::from( + "{\"error\":\"malformed JSON request\"}", + ))) + .unwrap()) + } + } +} + +fn parse_content_type(headers: &hyper::HeaderMap) -> Option { + let ct = headers.get("content-type")?.to_str().ok()?; + if ct.starts_with("application/x-protobuf") { + Some(ContentType::Protobuf) + } else if ct.starts_with("application/json") { + Some(ContentType::Json) + } else { + None + } +} diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 177d640fd8..a96da030c0 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -4547,6 +4547,7 @@ fn rewrite_forward_request( // Rebuild headers, stripping hop-by-hop and adding proxy headers let mut output = Vec::with_capacity(header_end + 128); let mut has_via = false; + let mut has_traceparent = false; for (i, line) in lines.iter().enumerate() { if i == 0 { @@ -4608,6 +4609,9 @@ fn rewrite_forward_request( if field_name == "via" { has_via = true; } + if field_name == "traceparent" { + has_traceparent = true; + } } // Generate the only Host field from the absolute request-target authority. @@ -4625,6 +4629,15 @@ fn rewrite_forward_request( if !has_via { output.extend_from_slice(b"Via: 1.1 openshell-sandbox\r\n"); } + if !has_traceparent { + let mut headers = http::HeaderMap::new(); + openshell_otel::propagation::inject_traceparent_if_missing(&mut headers); + if let Some(value) = headers.get("traceparent") { + output.extend_from_slice(b"traceparent: "); + output.extend_from_slice(value.as_bytes()); + output.extend_from_slice(b"\r\n"); + } + } // End of headers output.extend_from_slice(b"\r\n"); diff --git a/crates/openshell-supervisor-process/src/child_env.rs b/crates/openshell-supervisor-process/src/child_env.rs index 32eecbee35..bceedc0746 100644 --- a/crates/openshell-supervisor-process/src/child_env.rs +++ b/crates/openshell-supervisor-process/src/child_env.rs @@ -39,6 +39,19 @@ pub fn tls_env_vars( ] } +pub fn otel_env_vars(endpoint: &str, protocol: &str) -> [(&'static str, String); 2] { + [ + ( + openshell_core::sandbox_env::OTEL_EXPORTER_OTLP_ENDPOINT, + endpoint.to_owned(), + ), + ( + openshell_core::sandbox_env::OTEL_EXPORTER_OTLP_PROTOCOL, + protocol.to_owned(), + ), + ] +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 0ab1dd3187..e514acf72a 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -829,6 +829,13 @@ impl ProcessHandle { } } + // Set OTEL env vars so agent SDKs export to the supervisor's OTLP receiver + if netns_fd.is_some() { + for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { + cmd.env(key, value); + } + } + // Probe Landlock availability and emit OCSF logs from the parent // process where the tracing subscriber is functional. The child's // pre_exec context cannot reliably emit structured logs. diff --git a/crates/openshell-supervisor-process/src/run.rs b/crates/openshell-supervisor-process/src/run.rs index 8c47e789ba..2fa4510678 100644 --- a/crates/openshell-supervisor-process/src/run.rs +++ b/crates/openshell-supervisor-process/src/run.rs @@ -90,6 +90,7 @@ pub async fn run_process( tokio::sync::mpsc::UnboundedSender, >, #[cfg(target_os = "linux")] bypass_activity_tx: Option, + telemetry_rx: Option>, ) -> Result { // Platform drivers with a resolved numeric UID/GID retain the legacy // account-file update. OCI-image identity leaves those environment values @@ -378,6 +379,7 @@ pub async fn run_process( None, Arc::clone(&supervisor_terminating), main_instance_id.clone(), + telemetry_rx, ); info!("supervisor session task spawned"); Some(task) diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index fbd6d9275b..f95c67e966 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -1222,6 +1222,9 @@ fn apply_child_env( for (key, value) in child_env::proxy_env_vars(url) { cmd.env(key, value); } + for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { + cmd.env(key, value); + } } if let Some((ca_cert_path, combined_bundle_path)) = ca_file_paths { diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 98a3c0497b..50febc0195 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -274,6 +274,10 @@ fn map_session_stream_message( /// /// The task runs for the lifetime of the sandbox process, reconnecting with /// exponential backoff on failures. +/// +/// `telemetry_rx` is an optional channel for receiving telemetry messages from +/// the relay forwarder. The session drains this channel and forwards the +/// messages to the gateway. pub fn spawn( endpoint: String, sandbox_id: String, @@ -282,6 +286,7 @@ pub fn spawn( expected_ssh_peer_pid: Option, terminating: Arc, instance_id: String, + telemetry_rx: Option>, ) -> tokio::task::JoinHandle<()> { let config = SessionConfig { endpoint, @@ -291,6 +296,7 @@ pub fn spawn( expected_ssh_peer_pid, terminating, instance_id, + telemetry_rx, }; tokio::spawn(run_session_loop(config)) } @@ -303,16 +309,18 @@ struct SessionConfig { expected_ssh_peer_pid: Option, terminating: Arc, instance_id: String, + telemetry_rx: Option>, } -async fn run_session_loop(config: SessionConfig) { +async fn run_session_loop(mut config: SessionConfig) { let mut backoff = INITIAL_BACKOFF; let mut attempt: u64 = 0; + let mut telemetry_rx = config.telemetry_rx.take(); loop { attempt += 1; - match run_single_session(&config).await { + match run_single_session(&config, &mut telemetry_rx).await { Ok(()) => { let event = session_closed_event( openshell_ocsf::ctx::ctx(), @@ -339,6 +347,7 @@ async fn run_session_loop(config: SessionConfig) { async fn run_single_session( config: &SessionConfig, + telemetry_rx: &mut Option>, ) -> Result<(), Box> { // Connect to the gateway. The same `Channel` is used for both the // long-lived control stream and all data-plane `RelayStream` calls, so @@ -358,6 +367,7 @@ async fn run_single_session( payload: Some(supervisor_message::Payload::Hello(SupervisorHello { sandbox_id: config.sandbox_id.clone(), instance_id: config.instance_id.clone(), + capabilities: vec!["telemetry_relay".to_string()], })), }) .await @@ -384,6 +394,8 @@ async fn run_single_session( _ => return Err("expected SessionAccepted or SessionRejected".into()), }; + let telemetry_confirmed = accepted.capabilities.iter().any(|c| c == "telemetry_relay"); + let heartbeat_secs = accepted.heartbeat_interval_secs.max(5); let event = session_established_event( openshell_ocsf::ctx::ctx(), @@ -392,7 +404,8 @@ async fn run_single_session( heartbeat_secs, ); ocsf_emit!(event); - // Main loop: receive gateway messages + send heartbeats. + + // Main loop: receive gateway messages + send heartbeats + drain telemetry. let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(u64::from(heartbeat_secs))); heartbeat_interval.tick().await; // skip immediate tick @@ -432,6 +445,16 @@ async fn run_single_session( return Err("outbound channel closed".into()); } } + telemetry_msg = async { + match telemetry_rx.as_mut() { + Some(rx) => rx.recv().await, + None => std::future::pending().await, + } + }, if telemetry_confirmed => { + if let Some(msg) = telemetry_msg { + let _ = tx.try_send(msg); + } + } } } } diff --git a/mise.lock b/mise.lock index 597f54fecb..1d8fe34fee 100644 --- a/mise.lock +++ b/mise.lock @@ -134,6 +134,18 @@ checksum = "sha256:b8514ed7552e148b0a032114f745118dcb801791adafafeaf9935e4bfb0ed url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-pc-windows-msvc.zip" url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060720" +[[tools."github:mozilla/sccache"]] +version = "0.16.0" +backend = "github:mozilla/sccache" + +[tools."github:mozilla/sccache".options] +asset_pattern = "sccache-v*x86_64*linux*.tar.gz" + +[tools."github:mozilla/sccache"."platforms.linux-x64"] +checksum = "sha256:aec995a83ad3dff3d14b6314e08858b7b73d35ca85a5bcf3d3a9ec07dee35588" +url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682" + [[tools."github:rust-cross/cargo-zigbuild"]] version = "0.22.3" backend = "github:rust-cross/cargo-zigbuild" @@ -399,15 +411,19 @@ backend = "aqua:GoogleContainerTools/skaffold" "vars.version_prefix" = "v" [tools.skaffold."platforms.linux-arm64"] +checksum = "sha256:0062e1ff61f35200168454fa5a836306dd30b51acf30999d5d8836770d70ad2f" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-arm64" [tools.skaffold."platforms.linux-x64"] +checksum = "sha256:de75d5697d4a14d6a5e87105671ef9a30990270ab14ad301913f8f8d41a1ecca" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-amd64" [tools.skaffold."platforms.macos-arm64"] +checksum = "sha256:73478a431c17674d2f75d2c6e2fcbf35aa019de4ad6027e5573cc0f581767fbc" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-darwin-arm64" [tools.skaffold."platforms.windows-x64"] +checksum = "sha256:a257da2988782b4c1971bb58b9c7bb52176f8a0b587b6520dec0064f271fa416" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-windows-amd64.exe" [[tools.uv]] diff --git a/proto/openshell.proto b/proto/openshell.proto index e5e47b44e8..2fbc257eab 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2358,6 +2358,22 @@ message GetSandboxLogsResponse { // Supervisor session messages // --------------------------------------------------------------------------- +// Telemetry data forwarded from supervisor to gateway. +// Contains agent trace spans and/or OCSF events from a single sandbox. +message TelemetryData { + // Source sandbox identifier. + string sandbox_id = 1; + + // Serialized OTLP ExportTraceServiceRequest (protobuf encoding). + // Contains pre-enriched spans with sandbox resource attributes. + // Empty when only OCSF events are being forwarded. + bytes trace_data = 2; + + // Serialized OCSF events (each entry is a JSON-encoded OCSF event). + // Empty when only trace data is being forwarded. + repeated bytes ocsf_events = 3; +} + // Envelope for supervisor-to-gateway messages on the ConnectSupervisor stream. message SupervisorMessage { oneof payload { @@ -2365,6 +2381,7 @@ message SupervisorMessage { SupervisorHeartbeat heartbeat = 2; RelayOpenResult relay_open_result = 3; RelayClose relay_close = 4; + TelemetryData telemetry = 5; } } @@ -2385,6 +2402,8 @@ message SupervisorHello { string sandbox_id = 1; // Supervisor instance ID (e.g. boot id or process epoch). string instance_id = 2; + // Capabilities this supervisor supports (e.g. "telemetry_relay"). + repeated string capabilities = 3; } // Gateway accepts the supervisor session. @@ -2393,6 +2412,8 @@ message SessionAccepted { string session_id = 1; // Recommended heartbeat interval in seconds. uint32 heartbeat_interval_secs = 2; + // Capabilities confirmed by the gateway (subset of what supervisor advertised). + repeated string capabilities = 3; } // Gateway rejects the supervisor session. From 144f674625c0d36d6906efb0a538107942a84973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 08:16:29 +0200 Subject: [PATCH 2/8] fix(observability): address review findings in OTLP relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix Critical and Important findings from multi-agent code review: - Fix shutdown deadlock: drop telemetry sender before awaiting forwarder to allow the mpsc channel to close - Limit OTLP request body to 4 MiB (http_body_util::Limited), return 413 Payload Too Large when exceeded - Replace expect() with ConnectError on malformed OTLP endpoint URI to prevent gateway panic on startup - Add 10s timeout on telemetry export gRPC calls to prevent unbounded task accumulation during collector brownout - Validate OCSF events as JSON before logging and emit as structured field to prevent log injection via embedded newlines - Cap concurrent OTLP receiver connections at 64 via semaphore - Strip existing trusted keys before enrichment to prevent agent-supplied attribute spoofing - Fix rate limiter TOCTOU race with fetch_update CAS loop - Log session drop counter during shutdown for observability - Log on supervisor session try_send failure instead of silent discard Signed-off-by: Roland Huß --- .../src/supervisor_session.rs | 46 +++++++++++++++---- .../openshell-server/src/telemetry_relay.rs | 15 ++++-- .../src/otlp/enrichment.rs | 9 ++++ .../src/otlp/mod.rs | 20 +++++--- .../src/otlp/receiver.rs | 27 +++++++++-- .../src/supervisor_session.rs | 4 +- 6 files changed, 100 insertions(+), 21 deletions(-) diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index ec320dc5ea..5754b19aa6 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -1062,24 +1062,54 @@ fn handle_telemetry_data( let trace_data = telemetry.trace_data; let sandbox_id = sandbox_id.to_string(); tokio::spawn(async move { - if let Err(e) = exporter.export_raw(trace_data).await { - debug!( - sandbox_id = %sandbox_id, - error = %e, - "telemetry relay: failed to export trace data" - ); + match tokio::time::timeout( + Duration::from_secs(10), + exporter.export_raw(trace_data), + ) + .await + { + Ok(Err(e)) => { + debug!( + sandbox_id = %sandbox_id, + error = %e, + "telemetry relay: failed to export trace data" + ); + } + Err(_) => { + debug!( + sandbox_id = %sandbox_id, + "telemetry relay: export timed out" + ); + } + Ok(Ok(())) => {} } }); } + const MAX_OCSF_EVENT_SIZE: usize = 256 * 1024; for ocsf_event in &telemetry.ocsf_events { + if ocsf_event.len() > MAX_OCSF_EVENT_SIZE { + debug!( + sandbox_id = %sandbox_id, + size = ocsf_event.len(), + "telemetry relay: OCSF event too large, skipping" + ); + continue; + } if let Ok(json_str) = std::str::from_utf8(ocsf_event) { + if serde_json::from_str::(json_str).is_err() { + debug!( + sandbox_id = %sandbox_id, + "telemetry relay: OCSF event is not valid JSON, skipping" + ); + continue; + } info!( target: "ocsf_relay", sandbox_id = %sandbox_id, session_id = %session_id, - "{}", - json_str + ocsf_json = %json_str, + "relayed OCSF event" ); } } diff --git a/crates/openshell-server/src/telemetry_relay.rs b/crates/openshell-server/src/telemetry_relay.rs index 57abe75de5..7701ce9e08 100644 --- a/crates/openshell-server/src/telemetry_relay.rs +++ b/crates/openshell-server/src/telemetry_relay.rs @@ -24,11 +24,12 @@ pub struct TelemetryRelayExporter { impl TelemetryRelayExporter { /// Connect to the OTLP collector at the given gRPC endpoint. - pub async fn connect(endpoint: &str) -> Result { + pub async fn connect(endpoint: &str) -> Result { let channel = Channel::from_shared(endpoint.to_string()) - .expect("valid OTLP endpoint URL") + .map_err(|e| ConnectError::InvalidUri(e.to_string()))? .connect() - .await?; + .await + .map_err(ConnectError::Transport)?; Ok(Self { client: TraceServiceClient::new(channel), }) @@ -57,6 +58,14 @@ pub enum ExportError { Grpc(tonic::Status), } +#[derive(Debug, thiserror::Error)] +pub enum ConnectError { + #[error("invalid OTLP endpoint URI: {0}")] + InvalidUri(String), + #[error("transport error: {0}")] + Transport(tonic::transport::Error), +} + /// Create a relay exporter from the gateway's OTLP config, if configured. pub async fn try_create_exporter( config_file: Option<&crate::config_file::ConfigFile>, diff --git a/crates/openshell-supervisor-network/src/otlp/enrichment.rs b/crates/openshell-supervisor-network/src/otlp/enrichment.rs index d369583c7e..4078028f0e 100644 --- a/crates/openshell-supervisor-network/src/otlp/enrichment.rs +++ b/crates/openshell-supervisor-network/src/otlp/enrichment.rs @@ -38,11 +38,20 @@ pub fn enrich_spans( let extra_attrs = build_attributes(attrs, enrichment_enabled); + let trusted_keys: Vec<&str> = extra_attrs + .iter() + .filter_map(|a| a.key.as_str().into()) + .collect(); + for resource_spans in &mut request.resource_spans { let resource = resource_spans .resource .get_or_insert_with(Resource::default); + resource + .attributes + .retain(|a| !trusted_keys.contains(&a.key.as_str())); + for attr in &extra_attrs { resource.attributes.push(attr.clone()); } diff --git a/crates/openshell-supervisor-network/src/otlp/mod.rs b/crates/openshell-supervisor-network/src/otlp/mod.rs index 1bd08743eb..bb83581668 100644 --- a/crates/openshell-supervisor-network/src/otlp/mod.rs +++ b/crates/openshell-supervisor-network/src/otlp/mod.rs @@ -74,9 +74,12 @@ impl RateLimitedOcsfSink { let new_tokens = (elapsed.as_secs_f64() * f64::from(self.max_tokens)) as u32; if new_tokens > 0 { *last = now; - let current = self.tokens.load(Ordering::Relaxed); - let capped = (current + new_tokens).min(self.max_tokens); - self.tokens.store(capped, Ordering::Relaxed); + let max = self.max_tokens; + self.tokens + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + Some(current.saturating_add(new_tokens).min(max)) + }) + .ok(); } } @@ -131,6 +134,7 @@ pub struct RelayHandle { shutdown_tx: tokio::sync::oneshot::Sender<()>, forwarder_handle: tokio::task::JoinHandle<()>, receiver_handle: tokio::task::JoinHandle<()>, + session_drop_counter: Arc, pub telemetry_tx: TelemetrySender, } @@ -141,10 +145,12 @@ impl RelayHandle { let metrics = self.telemetry_tx.metrics().clone(); let _ = self.shutdown_tx.send(()); let _ = self.receiver_handle.await; + drop(self.telemetry_tx); let _ = self.forwarder_handle.await; info!( - spans_dropped = metrics.drops(), + buffer_drops = metrics.drops(), queue_depth = metrics.depth(), + session_drops = self.session_drop_counter.load(Ordering::Relaxed), "telemetry relay shut down" ); } @@ -191,7 +197,7 @@ impl TelemetryRelay { buf_rx, self.session_tx, self.sandbox_id.clone(), - session_drop_counter, + session_drop_counter.clone(), ); info!( @@ -204,6 +210,7 @@ impl TelemetryRelay { shutdown_tx, forwarder_handle, receiver_handle, + session_drop_counter, telemetry_tx: buf_tx, } } @@ -230,7 +237,7 @@ impl TelemetryRelay { buf_rx, self.session_tx, self.sandbox_id.clone(), - session_drop_counter, + session_drop_counter.clone(), ); info!( @@ -244,6 +251,7 @@ impl TelemetryRelay { shutdown_tx, forwarder_handle, receiver_handle, + session_drop_counter, telemetry_tx: buf_tx, }) } diff --git a/crates/openshell-supervisor-network/src/otlp/receiver.rs b/crates/openshell-supervisor-network/src/otlp/receiver.rs index 69e3ee1bd8..3d2201a303 100644 --- a/crates/openshell-supervisor-network/src/otlp/receiver.rs +++ b/crates/openshell-supervisor-network/src/otlp/receiver.rs @@ -5,6 +5,7 @@ use std::convert::Infallible; use std::net::SocketAddr; +use std::sync::Arc; use bytes::Bytes; use http_body_util::Full; @@ -52,6 +53,8 @@ pub async fn spawn_receiver( )) } +const MAX_CONCURRENT_CONNECTIONS: usize = 64; + fn spawn_receiver_inner( listener: TcpListener, buf_tx: TelemetrySender, @@ -61,6 +64,7 @@ fn spawn_receiver_inner( ) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { let mut shutdown_rx = shutdown_rx; + let conn_semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_CONNECTIONS)); loop { tokio::select! { @@ -70,6 +74,14 @@ fn spawn_receiver_inner( openshell_core::net::set_tcp_nodelay_best_effort(&stream); let buf_tx = buf_tx.clone(); let metadata = metadata.clone(); + let permit = match conn_semaphore.clone().try_acquire_owned() { + Ok(permit) => permit, + Err(_) => { + debug!("OTLP receiver: max concurrent connections reached, dropping"); + drop(stream); + continue; + } + }; tokio::spawn(async move { let svc = service_fn(move |req| { let buf_tx = buf_tx.clone(); @@ -84,6 +96,7 @@ fn spawn_receiver_inner( { debug!(error = %e, "OTLP HTTP connection error"); } + drop(permit); }); } Err(e) => { @@ -122,12 +135,20 @@ async fn handle_request( .unwrap()); }; - let body = match http_body_util::BodyExt::collect(req.into_body()).await { + const MAX_BODY_SIZE: usize = 4 * 1024 * 1024; // 4 MiB + let limited = http_body_util::Limited::new(req.into_body(), MAX_BODY_SIZE); + let body = match http_body_util::BodyExt::collect(limited).await { Ok(collected) => collected.to_bytes(), Err(e) => { - warn!(error = %e, "failed to read OTLP request body"); + let status = if e.to_string().contains("length limit exceeded") { + warn!("OTLP request body exceeds 4 MiB limit"); + StatusCode::PAYLOAD_TOO_LARGE + } else { + warn!(error = %e, "failed to read OTLP request body"); + StatusCode::BAD_REQUEST + }; return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) + .status(status) .body(Full::new(Bytes::from( "{\"error\":\"failed to read body\"}", ))) diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 50febc0195..6cbc8442fd 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -452,7 +452,9 @@ async fn run_single_session( } }, if telemetry_confirmed => { if let Some(msg) = telemetry_msg { - let _ = tx.try_send(msg); + if tx.try_send(msg).is_err() { + debug!("telemetry: session channel full or closed, dropping message"); + } } } } From b7881f9a958f2b6dbea8571561fc5ed02444c0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 08:31:57 +0200 Subject: [PATCH 3/8] docs(observability): clarify relay port binding and topology awareness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify in architecture/sandbox.md that the OTLP receiver only binds port 4318 when the relay is active (gateway has OTLP configured and confirms the telemetry_relay capability). Document topology constraints: all current topologies keep the process supervisor co-located with the agent, so 127.0.0.1 is correct. Note that future topologies moving the process supervisor out of the workload pod would need a different address. Remove the netns_fd gate on OTEL env var injection in process.rs since all topologies keep the supervisor co-located and the env vars are harmless when the relay is not running (OTel SDKs handle unreachable endpoints gracefully). Signed-off-by: Roland Huß --- architecture/sandbox.md | 22 ++++++++++++++----- .../src/process.rs | 10 ++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 5847d6e316..65bd1b1b74 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -481,13 +481,25 @@ Agent process --> OTLP HTTP (127.0.0.1:4318) --> Supervisor receiver ### Receiver Binding -The OTLP HTTP receiver always binds to `127.0.0.1:4318`. For Docker/Podman -drivers where the supervisor creates a network namespace, the bind happens -inside the namespace via `bind_tcp_in_netns()`. For Kubernetes and VM drivers, -the supervisor and agent share the same network namespace, so a direct bind -suffices. The receiver accepts both `application/x-protobuf` and +The OTLP HTTP receiver binds to `127.0.0.1:4318` only when the relay is +active (the gateway has `[openshell.gateway.otlp]` configured and confirms +the `telemetry_relay` capability). When OTLP is not configured, no port is +bound and no receiver runs. + +The bind address depends on the supervisor topology. In all current +topologies, the process supervisor runs co-located with the agent workload, +so `127.0.0.1` is reachable from the agent. For Docker/Podman drivers +where the supervisor creates a network namespace, the bind happens inside +the namespace via `bind_tcp_in_netns()`. For Kubernetes and VM drivers, +the supervisor and agent share the same network namespace, so a direct +bind suffices. The receiver accepts both `application/x-protobuf` and `application/json` content types. +Future topologies where the process supervisor moves out of the workload +pod would require the bind address and the `OTEL_EXPORTER_OTLP_ENDPOINT` +env var to reflect the supervisor's reachable address from the agent's +perspective (e.g., a service IP or pod IP). + ### Span Enrichment Forwarded spans are enriched with sandbox resource attributes: diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index e514acf72a..5e80cc51bf 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -829,11 +829,11 @@ impl ProcessHandle { } } - // Set OTEL env vars so agent SDKs export to the supervisor's OTLP receiver - if netns_fd.is_some() { - for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { - cmd.env(key, value); - } + // Set OTEL env vars so agent SDKs export to the supervisor's OTLP receiver. + // The relay binds on 127.0.0.1:4318 inside the agent's network namespace + // (all current topologies keep the process supervisor co-located with the agent). + for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { + cmd.env(key, value); } // Probe Landlock availability and emit OCSF logs from the parent From e6cada92f10dc31c168cb8692865328f156794c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 08:44:46 +0200 Subject: [PATCH 4/8] fix: revert accidental podman driver and mise.lock changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cherry-pick conflict resolution for podman container.rs used --theirs which in a cherry-pick context takes the spike's version, not main's. This accidentally included unrelated spike changes (TLS secret prefix removals, SPIFFE mount changes). Revert to upstream/main's version since our PR has no podman changes. Also revert mise.lock changes that were auto-generated when the pre-commit hook installed tools. Signed-off-by: Roland Huß --- .../openshell-driver-podman/src/container.rs | 612 +++++++++++++++++- mise.lock | 16 - 2 files changed, 589 insertions(+), 39 deletions(-) diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 30e115ee48..a81ee13e1d 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -14,7 +14,6 @@ use openshell_core::{driver_mounts, proto_struct}; use serde::Serialize; use serde_json::Value; use std::collections::{BTreeMap, HashSet}; -#[cfg(target_os = "linux")] use std::path::Path; /// Returns `true` when `SELinux` is enabled (enforcing or permissive). @@ -54,6 +53,9 @@ const VOLUME_PREFIX: &str = "openshell-sandbox-"; /// Secret name prefix for per-sandbox gateway JWTs. const TOKEN_SECRET_PREFIX: &str = "openshell-token-"; const PROXY_AUTH_SECRET_PREFIX: &str = "openshell-proxy-auth-"; +const TLS_CA_SECRET_PREFIX: &str = "openshell-tls-ca-"; +const TLS_CERT_SECRET_PREFIX: &str = "openshell-tls-cert-"; +const TLS_KEY_SECRET_PREFIX: &str = "openshell-tls-key-"; /// Container-side mount paths for client TLS materials and the sandbox token. const TLS_CA_MOUNT_PATH: &str = openshell_core::driver_utils::TLS_CA_MOUNT_PATH; @@ -62,6 +64,9 @@ const TLS_KEY_MOUNT_PATH: &str = openshell_core::driver_utils::TLS_KEY_MOUNT_PAT const SANDBOX_TOKEN_MOUNT_PATH: &str = openshell_core::driver_utils::SANDBOX_TOKEN_MOUNT_PATH; const UPSTREAM_PROXY_AUTH_MOUNT_PATH: &str = openshell_core::driver_utils::UPSTREAM_PROXY_AUTH_MOUNT_PATH; +const PROXY_CA_MOUNT_PATH: &str = openshell_core::driver_utils::PROXY_CA_MOUNT_PATH; +const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str = + openshell_core::driver_utils::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR; /// Directory inside sandbox containers where the supervisor binary is mounted. const SUPERVISOR_MOUNT_DIR: &str = openshell_core::driver_utils::SUPERVISOR_CONTAINER_DIR; @@ -171,6 +176,16 @@ pub fn proxy_auth_secret_name(sandbox_id: &str) -> String { format!("{PROXY_AUTH_SECRET_PREFIX}{sandbox_id}") } +/// Build per-sandbox Podman secret names for TLS CA, cert, and key. +#[must_use] +pub fn tls_secret_names(sandbox_id: &str) -> [String; 3] { + [ + format!("{TLS_CA_SECRET_PREFIX}{sandbox_id}"), + format!("{TLS_CERT_SECRET_PREFIX}{sandbox_id}"), + format!("{TLS_KEY_SECRET_PREFIX}{sandbox_id}"), + ] +} + /// Truncate a container ID to 12 characters (standard short form). #[must_use] pub fn short_id(id: &str) -> String { @@ -216,6 +231,10 @@ struct ContainerSpec { /// via Podman's `host-gateway` magic so sandbox containers can reach /// the gateway server running on the host in rootless mode. hostadd: Vec, + /// Search domains written to `/etc/resolv.conf` by Podman. + dns_search: Vec, + /// Resolver options written to `/etc/resolv.conf` by Podman. + dns_option: Vec, netns: NetNS, // Matches libpod's network spec format, which is `{name: {opts}}` where // empty opts is a unit struct rather than `()`. Keep as a map so JSON @@ -229,6 +248,13 @@ struct ContainerSpec { /// Port mappings from host to container. Using `host_port=0` requests an /// ephemeral port, readable back from the inspect response. portmappings: Vec, + /// User namespace mode override (e.g. `auto`). + #[serde(skip_serializing_if = "Option::is_none")] + userns: Option, + /// UID/GID mapping options. Required for `userns = "auto"` — the Podman + /// API needs `AutoUserNs: true` alongside the namespace mode. + #[serde(skip_serializing_if = "Option::is_none")] + idmappings: Option, } /// A port mapping entry for the libpod `SpecGenerator`. @@ -328,6 +354,49 @@ struct NetNS { nsmode: String, } +#[derive(Serialize)] +struct UserNS { + nsmode: String, + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, +} + +#[derive(Serialize)] +struct IDMap { + container_id: u32, + host_id: u32, + size: u32, +} + +#[derive(Serialize)] +struct IDMappings { + #[serde(rename = "HostUIDMapping")] + host_uid_mapping: bool, + #[serde(rename = "HostGIDMapping")] + host_gid_mapping: bool, + #[serde(rename = "AutoUserNs")] + auto_user_ns: bool, + #[serde(rename = "UIDMap", skip_serializing_if = "Vec::is_empty")] + uid_map: Vec, + #[serde(rename = "GIDMap", skip_serializing_if = "Vec::is_empty")] + gid_map: Vec, +} + +fn parse_id_maps(entries: &[String]) -> Result, ComputeDriverError> { + entries + .iter() + .map(|entry| { + let (cid, hid, size) = crate::config::parse_id_map_entry("idmap", entry) + .map_err(|err| ComputeDriverError::Precondition(err.to_string()))?; + Ok(IDMap { + container_id: cid, + host_id: hid, + size, + }) + }) + .collect() +} + #[derive(Serialize)] struct NetworkAttachment {} @@ -390,6 +459,12 @@ fn upstream_proxy_cli_args(config: &PodmanComputeConfig) -> Vec { if config.proxy_connect_by_hostname == Some(true) { args.push("--upstream-proxy-connect-by-hostname".to_string()); } + // A CA certificate is not secret, so the host PEM is bind-mounted + // read-only and the container-side mount path is passed on argv. + if config.proxy_ca_bundle.is_some() { + args.push("--upstream-proxy-ca-bundle".to_string()); + args.push(PROXY_CA_MOUNT_PATH.to_string()); + } args } @@ -454,14 +529,23 @@ fn build_env( config.sandbox_ssh_socket_path.clone(), ); env.insert("OPENSHELL_CONTAINER_IMAGE".into(), image.to_string()); + let main_process = openshell_core::sandbox_env::MainProcessConfig::encode_driver_spec(spec) + .expect("main process config serialization cannot fail"); env.insert( - openshell_core::sandbox_env::SANDBOX_COMMAND.into(), - "sleep infinity".into(), + openshell_core::sandbox_env::MAIN_PROCESS_SPEC.into(), + main_process, ); env.insert( openshell_core::sandbox_env::TELEMETRY_ENABLED.into(), openshell_core::telemetry::enabled_env_value().into(), ); + // Runtime capabilities are driver-owned. Override image/user input with + // only the substrate that this driver configures for the supervisor. + env.insert( + openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES.into(), + openshell_core::sandbox_env::POLICY_DNS_TRANSPARENT_TCP_CAPABILITY.into(), + ); + // 3. TLS client cert paths (when mTLS is enabled). These point to // the container-side mount paths where the cert files are // bind-mounted from the host. @@ -480,8 +564,20 @@ fn build_env( ); } + if let Some(socket_path) = provider_spiffe_workload_api_socket_env_value(config) { + env.insert( + openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET.into(), + socket_path, + ); + } + env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN); env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN_FILE); + // Prevent user-supplied environment from overriding the TLS server name + // the supervisor verifies — a sandbox user who can redirect the gateway + // hostname could otherwise present a certificate for a name they control + // and intercept the sandbox JWT. + env.remove(openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME); env.insert( openshell_core::sandbox_env::OCI_IMAGE_USER.into(), oci_user.to_string(), @@ -904,9 +1000,12 @@ pub fn build_container_spec_with_token_and_gpu_devices( image, image, "", + None, + None, ) } +#[allow(clippy::too_many_arguments)] pub fn build_container_spec_for_image( sandbox: &DriverSandbox, config: &PodmanComputeConfig, @@ -915,6 +1014,8 @@ pub fn build_container_spec_for_image( requested_image: &str, image_id: &str, oci_user: &str, + supervisor_bin_path: Option<&Path>, + tls_secret_names: Option<&[String; 3]>, ) -> Result { let name = container_name(&sandbox.workspace, &sandbox.name, &sandbox.id); let vol = volume_name(&sandbox.id); @@ -956,11 +1057,15 @@ pub fn build_container_spec_for_image( }]; volumes.extend(user_mounts.volumes); - let mut image_volumes = vec![ImageVolume { - source: config.supervisor_image.clone(), - destination: SUPERVISOR_MOUNT_DIR.into(), - rw: false, - }]; + let mut image_volumes = if supervisor_bin_path.is_some() { + Vec::new() + } else { + vec![ImageVolume { + source: config.supervisor_image.clone(), + destination: SUPERVISOR_MOUNT_DIR.into(), + rw: false, + }] + }; image_volumes.extend(user_mounts.image_volumes); let mut command = vec![ "--workdir".to_string(), @@ -1009,8 +1114,6 @@ pub fn build_container_spec_for_image( "DAC_OVERRIDE".into(), // Not needed: the supervisor does not create setuid/setgid executables. "FSETID".into(), - // Not needed: the supervisor does not send signals to arbitrary processes. - "KILL".into(), // Not needed: the supervisor does not bind privileged ports (<1024). "NET_BIND_SERVICE".into(), // Not in Podman's default set but explicitly denied in case the image @@ -1041,6 +1144,9 @@ pub fn build_container_spec_for_image( // Child setup clears the capability bounding set before exec, which // requires CAP_SETPCAP in the supervisor until drop_privileges(). "SETPCAP".into(), + // Forwarding shutdown signals to the canonical workload process + // group after it drops to the sandbox UID requires CAP_KILL. + "KILL".into(), ], // SETUID, SETGID, SETPCAP, CHOWN, and FOWNER are intentionally kept from // Podman's default set and not dropped: @@ -1110,6 +1216,29 @@ pub fn build_container_spec_for_image( mode: 0o400, }); } + if let Some([ca, cert, key]) = tls_secret_names { + secrets.push(SecretMount { + source: ca.clone(), + target: TLS_CA_MOUNT_PATH.into(), + uid: 0, + gid: 0, + mode: 0o400, + }); + secrets.push(SecretMount { + source: cert.clone(), + target: TLS_CERT_MOUNT_PATH.into(), + uid: 0, + gid: 0, + mode: 0o400, + }); + secrets.push(SecretMount { + source: key.clone(), + target: TLS_KEY_MOUNT_PATH.into(), + uid: 0, + gid: 0, + mode: 0o400, + }); + } secrets }, stop_timeout: config.stop_timeout_secs, @@ -1117,6 +1246,11 @@ pub fn build_container_spec_for_image( // reach services on the host. `host.openshell.internal` is the driver- // neutral alias used by policies and e2e tests. hostadd: hostadd_entries(config), + // Preserve Podman's resolver defaults for both policy-DNS and ordinary + // sandboxes. Namespace-local capture supports UDP and TCP, so it must + // not depend on a libc-specific option or alter short-name searches. + dns_search: Vec::new(), + dns_option: Vec::new(), netns: NetNS { nsmode: "bridge".to_string(), }, @@ -1135,20 +1269,20 @@ pub fn build_container_spec_for_image( destination: openshell_core::container_paths::NETNS_MOUNT_ROOT.into(), options: vec!["rw".into(), "nosuid".into(), "nodev".into()], }]; - // Bind-mount client TLS materials into the container when mTLS - // is enabled. The supervisor reads these via OPENSHELL_TLS_CA, - // OPENSHELL_TLS_CERT, and OPENSHELL_TLS_KEY env vars (set in - // build_env above) to establish an mTLS connection back to the - // gateway. - if let (Some(ca), Some(cert), Some(key)) = ( - &config.guest_tls_ca, - &config.guest_tls_cert, - &config.guest_tls_key, - ) { + // Deliver client TLS materials into the container when mTLS is + // enabled. When userns remaps UIDs (auto, no-map), bind-mounted + // host files are unreadable because the container root maps to a + // different host UID. In that case TLS materials are delivered as + // Podman secrets (handled in the `secrets` block above); otherwise + // use bind mounts. + if tls_secret_names.is_none() + && let (Some(ca), Some(cert), Some(key)) = ( + &config.guest_tls_ca, + &config.guest_tls_cert, + &config.guest_tls_key, + ) + { let mut ro = vec!["ro".into(), "rbind".into()]; - // On SELinux-enabled systems (Fedora, RHEL), bind-mounted - // files need the shared relabel option so the container - // process can read them through the SELinux MAC policy. if is_selinux_enabled() { ro.push("z".into()); } @@ -1171,6 +1305,47 @@ pub fn build_container_spec_for_image( options: ro, }); } + // Bind-mount the corporate proxy CA bundle read-only when + // configured. A CA certificate is not secret, so unlike the proxy + // credential (a driver secret) a plain read-only bind mount is + // used. The supervisor reads it via the --upstream-proxy-ca-bundle + // argv path (see upstream_proxy_cli_args) to verify an https:// + // proxy and trust re-signed upstream certificates. + if let Some(ca_bundle) = &config.proxy_ca_bundle { + let mut ro = vec!["ro".into(), "rbind".into()]; + if is_selinux_enabled() { + ro.push("z".into()); + } + m.push(Mount { + kind: "bind".into(), + source: ca_bundle.clone(), + destination: PROXY_CA_MOUNT_PATH.into(), + options: ro, + }); + } + if let Some(bin_path) = supervisor_bin_path { + let mut opts = vec!["ro".into(), "rbind".into()]; + if is_selinux_enabled() { + opts.push("z".into()); + } + m.push(Mount { + kind: "bind".into(), + source: bin_path.display().to_string(), + destination: SUPERVISOR_BINARY_PATH.into(), + options: opts, + }); + } + if let Some(path) = provider_spiffe_workload_api_socket_mount_source(config) { + // No SELinux relabel - the SPIRE agent socket is shared host + // infrastructure and must keep its existing SELinux context. + let ro = vec!["ro".into(), "rbind".into()]; + m.push(Mount { + kind: "bind".into(), + source: path.display().to_string(), + destination: PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR.into(), + options: ro, + }); + } m.extend(user_mounts.mounts); m }, @@ -1182,11 +1357,68 @@ pub fn build_container_spec_for_image( container_port: openshell_core::config::DEFAULT_SSH_PORT, protocol: "tcp".into(), }], + userns: config.userns.as_deref().map(|raw| { + let (base, params) = raw + .split_once(':') + .map_or((raw, None), |(b, p)| (b, Some(p))); + UserNS { + nsmode: base.to_string(), + value: params.map(ToString::to_string), + } + }), + idmappings: match config + .userns + .as_deref() + .map(|m| m.split(':').next().unwrap_or(m)) + { + Some("auto") => Some(IDMappings { + host_uid_mapping: false, + host_gid_mapping: false, + auto_user_ns: true, + uid_map: Vec::new(), + gid_map: Vec::new(), + }), + Some("private") => Some(IDMappings { + host_uid_mapping: false, + host_gid_mapping: false, + auto_user_ns: false, + uid_map: parse_id_maps(&config.uidmap)?, + gid_map: parse_id_maps(&config.gidmap)?, + }), + _ => None, + }, }; Ok(serde_json::to_value(container_spec).expect("ContainerSpec serialization cannot fail")) } +fn provider_spiffe_workload_api_socket_env_value(config: &PodmanComputeConfig) -> Option { + let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; + let raw = host_path.to_str()?; + if raw.starts_with("tcp:") { + return Some(raw.to_string()); + } + let host_path = raw + .strip_prefix("unix:") + .map_or(host_path.as_path(), Path::new); + let file_name = host_path.file_name()?.to_str()?; + Some(format!( + "{PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR}/{file_name}" + )) +} + +fn provider_spiffe_workload_api_socket_mount_source(config: &PodmanComputeConfig) -> Option<&Path> { + let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; + let raw = host_path.to_str()?; + if raw.starts_with("tcp:") { + return None; + } + let host_path = raw + .strip_prefix("unix:") + .map_or(host_path.as_path(), Path::new); + host_path.parent() +} + fn hostadd_entries(config: &PodmanComputeConfig) -> Vec { let host_gateway_ip = config.host_gateway_ip.trim(); if host_gateway_ip.is_empty() { @@ -1384,6 +1616,8 @@ mod tests { "registry.example/app:latest", "sha256:immutable", "app:staff", + None, + None, ) .unwrap(); @@ -1394,6 +1628,8 @@ mod tests { ); assert_eq!(container["user"].as_str(), Some("0:0")); assert_eq!(container["image_pull_policy"].as_str(), Some("never")); + assert_eq!(container["dns_search"], serde_json::json!([])); + assert_eq!(container["dns_option"], serde_json::json!([])); assert_eq!( container["env"][openshell_core::sandbox_env::OCI_IMAGE_USER].as_str(), Some("app:staff") @@ -1412,6 +1648,24 @@ mod tests { ); } + #[test] + fn build_env_strips_gateway_tls_server_name() { + let mut sandbox = test_sandbox("test-id", "test-name"); + let spec = sandbox.spec.get_or_insert_default(); + spec.environment.insert( + openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME.to_string(), + "evil.attacker.example.com".to_string(), + ); + + let container = build_container_spec(&sandbox, &test_config()); + + assert_eq!( + container["env"].get(openshell_core::sandbox_env::GATEWAY_TLS_SERVER_NAME), + None, + "GATEWAY_TLS_SERVER_NAME must be stripped from the supervisor environment" + ); + } + #[test] fn volume_name_uses_id() { assert_eq!( @@ -1632,6 +1886,7 @@ mod tests { "missing DAC_READ_SEARCH" ); assert!(added.contains(&"SETPCAP"), "missing SETPCAP"); + assert!(added.contains(&"KILL"), "missing KILL"); // SETUID and SETGID are NOT in cap_add — they remain available from the // default bounding set because we no longer use cap_drop:ALL. Verify they @@ -1647,6 +1902,10 @@ mod tests { .collect(); assert!(!dropped.contains(&"SETUID"), "SETUID must not be dropped"); assert!(!dropped.contains(&"SETGID"), "SETGID must not be dropped"); + assert!( + dropped.contains(&"NET_BIND_SERVICE"), + "NET_BIND_SERVICE must stay dropped; policy DNS binds an unprivileged port" + ); assert!( !dropped.contains(&"CHOWN"), "CHOWN must not be dropped (needed for prepare_filesystem chown)" @@ -1659,6 +1918,10 @@ mod tests { !dropped.contains(&"SETPCAP"), "SETPCAP must not be dropped (needed for child bounding-set clear)" ); + assert!( + !dropped.contains(&"KILL"), + "KILL must not be dropped (needed to signal the sandbox workload on shutdown)" + ); assert!( !dropped.contains(&"ALL"), "must not use cap_drop:ALL in rootless Podman" @@ -1807,6 +2070,26 @@ mod tests { ); } + #[test] + fn container_spec_keeps_network_capabilities_driver_controlled() { + use openshell_core::proto::compute::v1::{DriverSandboxSpec, DriverSandboxTemplate}; + + let mut sandbox = test_sandbox("test-id", "legit-name"); + sandbox.spec = Some(DriverSandboxSpec { + environment: std::collections::HashMap::from([( + openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES.to_string(), + "spoofed".to_string(), + )]), + template: Some(DriverSandboxTemplate::default()), + ..Default::default() + }); + let spec = build_container_spec(&sandbox, &test_config()); + assert_eq!( + spec["env"][openshell_core::sandbox_env::NETWORK_RUNTIME_CAPABILITIES], + serde_json::json!(openshell_core::sandbox_env::POLICY_DNS_TRANSPARENT_TCP_CAPABILITY) + ); + } + /// Extract the container spec's supervisor argv (`command`) as strings. fn spec_command(spec: &Value) -> Vec { spec["command"] @@ -1874,6 +2157,61 @@ mod tests { ); } + #[test] + fn container_spec_binds_proxy_ca_bundle_and_passes_argv() { + let sandbox = test_sandbox("ca-id", "ca-name"); + let mut config = test_config(); + config.https_proxy = Some("https://proxy.corp.com:3130".to_string()); + config.proxy_ca_bundle = Some("/host/proxy-ca.pem".to_string()); + + let spec = build_container_spec(&sandbox, &config); + let command = spec_command(&spec); + + // The container-side mount path travels on argv as a flag/value pair. + let idx = command + .iter() + .position(|a| a == "--upstream-proxy-ca-bundle") + .expect("CA bundle flag present"); + assert_eq!( + command.get(idx + 1).map(String::as_str), + Some("/etc/openshell/tls/proxy/ca-bundle.pem") + ); + + // The host PEM is bind-mounted read-only at that container path. + let mounts = spec["mounts"] + .as_array() + .expect("mounts should be an array"); + let ca_mount = mounts + .iter() + .find(|m| { + m["type"].as_str() == Some("bind") + && m["destination"].as_str() == Some("/etc/openshell/tls/proxy/ca-bundle.pem") + }) + .expect("CA bundle bind mount present"); + assert_eq!(ca_mount["source"].as_str(), Some("/host/proxy-ca.pem")); + assert!( + ca_mount["options"] + .as_array() + .expect("options array") + .iter() + .any(|o| o.as_str() == Some("ro")), + "CA bundle mount must be read-only" + ); + } + + #[test] + fn container_spec_omits_proxy_ca_bundle_when_unconfigured() { + let sandbox = test_sandbox("test-id", "test-name"); + let spec = build_container_spec(&sandbox, &test_config()); + let mounts = spec["mounts"].as_array().expect("mounts array"); + assert!( + !mounts.iter().any( + |m| m["destination"].as_str() == Some("/etc/openshell/tls/proxy/ca-bundle.pem") + ), + "no CA bundle mount without operator config" + ); + } + #[test] fn container_spec_sandbox_env_cannot_influence_proxy_argv() { use openshell_core::proto::compute::v1::{DriverSandboxSpec, DriverSandboxTemplate}; @@ -2764,6 +3102,33 @@ mod tests { ); } + #[test] + fn container_spec_includes_provider_spiffe_socket_when_configured() { + let sandbox = test_sandbox("spiffe-id", "spiffe-name"); + let mut config = test_config(); + config.provider_spiffe_workload_api_socket = + Some(std::path::PathBuf::from("/host/spire-agent.sock")); + + let spec = build_container_spec(&sandbox, &config); + + let env_map = spec["env"].as_object().expect("env should be an object"); + assert_eq!( + env_map + .get(openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET) + .and_then(|v| v.as_str()), + Some("/spiffe-workload-api/spire-agent.sock"), + ); + + let mounts = spec["mounts"] + .as_array() + .expect("mounts should be an array"); + assert!(mounts.iter().any(|m| { + m["type"].as_str() == Some("bind") + && m["source"].as_str() == Some("/host") + && m["destination"].as_str() == Some(PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR) + })); + } + #[test] fn container_spec_omits_tls_without_config() { let sandbox = test_sandbox("notls-id", "notls-name"); @@ -2786,4 +3151,205 @@ mod tests { .count(); assert_eq!(bind_count, 0, "no bind mounts without TLS config"); } + + #[test] + fn container_spec_includes_userns_when_configured() { + let sandbox = test_sandbox("userns-id", "userns-name"); + let mut config = test_config(); + config.userns = Some("auto".to_string()); + let spec = build_container_spec(&sandbox, &config); + + let userns = &spec["userns"]; + assert_eq!(userns["nsmode"].as_str(), Some("auto")); + assert!(userns.get("value").is_none(), "bare auto should omit value"); + + let idmappings = &spec["idmappings"]; + assert_eq!( + idmappings["AutoUserNs"].as_bool(), + Some(true), + "idmappings.AutoUserNs must be true for userns=auto" + ); + } + + #[test] + fn container_spec_auto_with_params() { + let sandbox = test_sandbox("userns-auto-params-id", "userns-auto-params-name"); + let mut config = test_config(); + config.userns = Some("auto:size=65536".to_string()); + let spec = build_container_spec(&sandbox, &config); + + let userns = &spec["userns"]; + assert_eq!(userns["nsmode"].as_str(), Some("auto")); + assert_eq!(userns["value"].as_str(), Some("size=65536")); + + assert_eq!( + spec["idmappings"]["AutoUserNs"].as_bool(), + Some(true), + "idmappings.AutoUserNs must be true for auto:size=65536" + ); + } + + #[test] + fn container_spec_keep_id_with_params() { + let sandbox = test_sandbox("userns-keepid-id", "userns-keepid-name"); + let mut config = test_config(); + config.userns = Some("keep-id:uid=1000,gid=1000".to_string()); + let spec = build_container_spec(&sandbox, &config); + + let userns = &spec["userns"]; + assert_eq!(userns["nsmode"].as_str(), Some("keep-id")); + assert_eq!(userns["value"].as_str(), Some("uid=1000,gid=1000")); + + assert!( + spec.get("idmappings").is_none(), + "idmappings should not be set for keep-id" + ); + } + + #[test] + fn container_spec_nomap_mode() { + let sandbox = test_sandbox("userns-nomap-id", "userns-nomap-name"); + let mut config = test_config(); + config.userns = Some("no-map".to_string()); + let spec = build_container_spec(&sandbox, &config); + + let userns = &spec["userns"]; + assert_eq!(userns["nsmode"].as_str(), Some("no-map")); + assert!(userns.get("value").is_none(), "no-map should omit value"); + + assert!( + spec.get("idmappings").is_none(), + "idmappings should not be set for no-map" + ); + } + + #[test] + fn container_spec_private_with_mappings() { + let sandbox = test_sandbox("userns-private-id", "userns-private-name"); + let mut config = test_config(); + config.userns = Some("private".to_string()); + config.uidmap = vec!["0:1000:1".to_string(), "1:100000:65536".to_string()]; + config.gidmap = vec!["0:1000:1".to_string(), "1:100000:65536".to_string()]; + let spec = build_container_spec(&sandbox, &config); + + let userns = &spec["userns"]; + assert_eq!(userns["nsmode"].as_str(), Some("private")); + assert!(userns.get("value").is_none(), "private should omit value"); + + let idmappings = &spec["idmappings"]; + assert_eq!( + idmappings["AutoUserNs"].as_bool(), + Some(false), + "AutoUserNs must be false for private mode" + ); + + let uid_map = idmappings["UIDMap"] + .as_array() + .expect("UIDMap should be an array"); + assert_eq!(uid_map.len(), 2); + assert_eq!(uid_map[0]["container_id"].as_u64(), Some(0)); + assert_eq!(uid_map[0]["host_id"].as_u64(), Some(1000)); + assert_eq!(uid_map[0]["size"].as_u64(), Some(1)); + assert_eq!(uid_map[1]["container_id"].as_u64(), Some(1)); + assert_eq!(uid_map[1]["host_id"].as_u64(), Some(100_000)); + assert_eq!(uid_map[1]["size"].as_u64(), Some(65536)); + + let gid_map = idmappings["GIDMap"] + .as_array() + .expect("GIDMap should be an array"); + assert_eq!(gid_map.len(), 2); + assert_eq!(gid_map[0]["container_id"].as_u64(), Some(0)); + assert_eq!(gid_map[0]["host_id"].as_u64(), Some(1000)); + assert_eq!(gid_map[0]["size"].as_u64(), Some(1)); + } + + #[test] + fn container_spec_omits_userns_when_unset() { + let sandbox = test_sandbox("no-userns-id", "no-userns-name"); + let config = test_config(); + let spec = build_container_spec(&sandbox, &config); + + assert!( + spec.get("userns").is_none(), + "userns should not be set when unconfigured" + ); + assert!( + spec.get("idmappings").is_none(), + "idmappings should not be set when userns is unconfigured" + ); + } + + #[test] + fn container_spec_uses_bind_mount_for_supervisor_when_path_provided() { + let sandbox = test_sandbox("bind-sv-id", "bind-sv-name"); + let config = test_config(); + let image = resolve_image(&sandbox, &config); + let spec = build_container_spec_for_image( + &sandbox, + &config, + None, + None, + image, + image, + "", + Some(Path::new("/host/cache/openshell-sandbox")), + None, + ) + .unwrap(); + + let image_volumes = spec["image_volumes"] + .as_array() + .expect("image_volumes should be an array"); + assert!( + !image_volumes + .iter() + .any(|v| v["destination"].as_str() == Some(SUPERVISOR_MOUNT_DIR)), + "supervisor image volume should not be present when bind path is provided" + ); + + let mounts = spec["mounts"] + .as_array() + .expect("mounts should be an array"); + let sv_bind = mounts + .iter() + .find(|m| m["destination"].as_str() == Some(SUPERVISOR_BINARY_PATH)); + assert!( + sv_bind.is_some(), + "supervisor bind mount should be present at {SUPERVISOR_BINARY_PATH}" + ); + let sv_bind = sv_bind.unwrap(); + assert_eq!( + sv_bind["source"].as_str(), + Some("/host/cache/openshell-sandbox") + ); + assert_eq!(sv_bind["type"].as_str(), Some("bind")); + } + + #[test] + fn container_spec_uses_image_volume_when_no_bind_path() { + let sandbox = test_sandbox("imgvol-id", "imgvol-name"); + let config = test_config(); + let spec = build_container_spec(&sandbox, &config); + + let image_volumes = spec["image_volumes"] + .as_array() + .expect("image_volumes should be an array"); + assert!( + image_volumes + .iter() + .any(|v| v["destination"].as_str() == Some(SUPERVISOR_MOUNT_DIR)), + "supervisor image volume should be present by default" + ); + + let mounts = spec["mounts"] + .as_array() + .expect("mounts should be an array"); + assert!( + !mounts.iter().any( + |m| m["destination"].as_str() == Some(SUPERVISOR_BINARY_PATH) + && m["type"].as_str() == Some("bind") + ), + "supervisor bind mount should not be present by default" + ); + } } diff --git a/mise.lock b/mise.lock index 1d8fe34fee..597f54fecb 100644 --- a/mise.lock +++ b/mise.lock @@ -134,18 +134,6 @@ checksum = "sha256:b8514ed7552e148b0a032114f745118dcb801791adafafeaf9935e4bfb0ed url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-pc-windows-msvc.zip" url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060720" -[[tools."github:mozilla/sccache"]] -version = "0.16.0" -backend = "github:mozilla/sccache" - -[tools."github:mozilla/sccache".options] -asset_pattern = "sccache-v*x86_64*linux*.tar.gz" - -[tools."github:mozilla/sccache"."platforms.linux-x64"] -checksum = "sha256:aec995a83ad3dff3d14b6314e08858b7b73d35ca85a5bcf3d3a9ec07dee35588" -url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682" - [[tools."github:rust-cross/cargo-zigbuild"]] version = "0.22.3" backend = "github:rust-cross/cargo-zigbuild" @@ -411,19 +399,15 @@ backend = "aqua:GoogleContainerTools/skaffold" "vars.version_prefix" = "v" [tools.skaffold."platforms.linux-arm64"] -checksum = "sha256:0062e1ff61f35200168454fa5a836306dd30b51acf30999d5d8836770d70ad2f" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-arm64" [tools.skaffold."platforms.linux-x64"] -checksum = "sha256:de75d5697d4a14d6a5e87105671ef9a30990270ab14ad301913f8f8d41a1ecca" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-amd64" [tools.skaffold."platforms.macos-arm64"] -checksum = "sha256:73478a431c17674d2f75d2c6e2fcbf35aa019de4ad6027e5573cc0f581767fbc" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-darwin-arm64" [tools.skaffold."platforms.windows-x64"] -checksum = "sha256:a257da2988782b4c1971bb58b9c7bb52176f8a0b587b6520dec0064f271fa416" url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-windows-amd64.exe" [[tools.uv]] From cb274db399213a1d55c89458d9f63cb242bda154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 08:55:23 +0200 Subject: [PATCH 5/8] refactor(observability): extract OTLP receiver address constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded "127.0.0.1:4318" with constants in sandbox_env.rs (OTLP_RECEIVER_ADDR and OTLP_RECEIVER_ENDPOINT) so the bind address and agent env var are defined in one place. Future topologies that move the process supervisor out of the agent's network namespace can update these constants or derive the address from the topology. Signed-off-by: Roland Huß --- crates/openshell-core/src/sandbox_env.rs | 9 ++++++++ .../openshell-driver-kubernetes/src/driver.rs | 2 +- crates/openshell-sandbox/src/lib.rs | 21 ++++++++++++------- .../src/process.rs | 2 +- .../openshell-supervisor-process/src/ssh.rs | 2 +- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/openshell-core/src/sandbox_env.rs b/crates/openshell-core/src/sandbox_env.rs index ab93c4aadd..0f12501323 100644 --- a/crates/openshell-core/src/sandbox_env.rs +++ b/crates/openshell-core/src/sandbox_env.rs @@ -247,6 +247,15 @@ pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; /// Set to `http/protobuf` when the telemetry relay is active. pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; +/// Default OTLP receiver bind address and port. All current topologies keep the +/// process supervisor co-located with the agent, so localhost is correct. Future +/// topologies that move the supervisor out of the agent's network namespace +/// would derive the address from the topology (e.g., pod IP via downward API). +pub const OTLP_RECEIVER_ADDR: &str = "127.0.0.1:4318"; + +/// Default OTLP receiver endpoint URL for agent env var injection. +pub const OTLP_RECEIVER_ENDPOINT: &str = "http://127.0.0.1:4318"; + // The corporate upstream-proxy configuration deliberately has no reserved // environment variables: it travels on the supervisor's argv // (`--upstream-proxy` and friends), which a sandbox image cannot forge the diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index d385f640f5..3dcc755c8c 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -4344,7 +4344,7 @@ fn build_env_list( upsert_env( &mut env, openshell_core::sandbox_env::OTEL_EXPORTER_OTLP_ENDPOINT, - "http://127.0.0.1:4318", + openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, ); upsert_env( &mut env, diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 20948fb159..77e95b281b 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -955,12 +955,17 @@ pub async fn run_sandbox( None }; - // Telemetry relay: bind OTLP receiver on 127.0.0.1:4318 for all - // Linux topologies. For Docker/Podman with a netns, bind inside the - // namespace. For K8s/VM (shared namespace), bind directly. + // Telemetry relay: bind OTLP receiver for all Linux topologies. + // All current topologies keep the process supervisor co-located with + // the agent, so 127.0.0.1 is reachable from agent processes. Future + // topologies that move the supervisor out of the workload pod would + // need to derive the bind address from the topology (e.g., pod IP + // via downward API) and update OTEL_EXPORTER_OTLP_ENDPOINT to match. let telemetry_rx = { #[cfg(target_os = "linux")] { + let otlp_addr = openshell_core::sandbox_env::OTLP_RECEIVER_ADDR; + let (telemetry_session_tx, telemetry_session_rx) = tokio::sync::mpsc::channel::(64); @@ -982,13 +987,13 @@ pub async fn run_sandbox( telemetry_session_tx, ); - let localhost_addr: std::net::SocketAddr = "127.0.0.1:4318".parse().unwrap(); + let bind_addr: std::net::SocketAddr = otlp_addr.parse().unwrap(); if let Some(ns) = netns.as_ref() { - match ns.bind_tcp_in_netns("127.0.0.1:4318").await { + match ns.bind_tcp_in_netns(otlp_addr).await { Ok(listener) => { let handle = relay.start_with_listener(listener); - tracing::info!(bind = %localhost_addr, "telemetry relay started (netns)"); + tracing::info!(bind = %bind_addr, "telemetry relay started (netns)"); telemetry_relay_handle = Some(handle); } Err(e) => { @@ -996,9 +1001,9 @@ pub async fn run_sandbox( } } } else { - match relay.start(localhost_addr).await { + match relay.start(bind_addr).await { Ok(handle) => { - tracing::info!(bind = %localhost_addr, "telemetry relay started"); + tracing::info!(bind = %bind_addr, "telemetry relay started"); telemetry_relay_handle = Some(handle); } Err(e) => { diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 5e80cc51bf..3f1b07cc52 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -832,7 +832,7 @@ impl ProcessHandle { // Set OTEL env vars so agent SDKs export to the supervisor's OTLP receiver. // The relay binds on 127.0.0.1:4318 inside the agent's network namespace // (all current topologies keep the process supervisor co-located with the agent). - for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { + for (key, value) in child_env::otel_env_vars(openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, "http/protobuf") { cmd.env(key, value); } diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index f95c67e966..2bc76a7a3e 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -1222,7 +1222,7 @@ fn apply_child_env( for (key, value) in child_env::proxy_env_vars(url) { cmd.env(key, value); } - for (key, value) in child_env::otel_env_vars("http://127.0.0.1:4318", "http/protobuf") { + for (key, value) in child_env::otel_env_vars(openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, "http/protobuf") { cmd.env(key, value); } } From db992a8fc79b4ff18fdf21e1e848132d0923b1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 09:04:04 +0200 Subject: [PATCH 6/8] test(observability): add unit tests for OTLP relay buffer, enrichment, and forwarder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 13 new tests bringing the relay test suite from 4 to 17 tests: Buffer (4 new): - depth_tracks_send_and_recv: queue depth accuracy across operations - drop_count_increments_on_each_overflow: counter accuracy under sustained overflow - metrics_shared_across_clones: shared metrics between sender clones - recv_returns_none_when_all_senders_dropped: channel close behavior Enrichment (4 new): - enrichment_strips_agent_supplied_trusted_keys: dedup prevents spoofing - enrichment_preserves_non_trusted_agent_attributes: custom attrs kept - enrichment_handles_json_content_type: JSON input, protobuf output - enrichment_rejects_invalid_protobuf: error handling for bad input Rate limiter (3 new): - rate_limiter_acquires_initial_tokens: exact token count - rate_limiter_drops_when_exhausted: OcsfRelaySink drop counting - rate_limiter_refills_after_time: time-based token refill Forwarder (2 new): - forwarder_constructs_telemetry_data_messages: correct message construction - forwarder_increments_session_drop_counter: session channel backpressure Signed-off-by: Roland Huß --- .../src/otlp/buffer.rs | 59 +++++++ .../src/otlp/enrichment.rs | 144 ++++++++++++++++++ .../src/otlp/mod.rs | 104 +++++++++++++ 3 files changed, 307 insertions(+) diff --git a/crates/openshell-supervisor-network/src/otlp/buffer.rs b/crates/openshell-supervisor-network/src/otlp/buffer.rs index acd97c2da9..56ab4079fd 100644 --- a/crates/openshell-supervisor-network/src/otlp/buffer.rs +++ b/crates/openshell-supervisor-network/src/otlp/buffer.rs @@ -149,4 +149,63 @@ mod tests { assert!(matches!(&items[1], TelemetryItem::Ocsf(_))); assert_eq!(rx.metrics().depth(), 0); } + + #[tokio::test] + async fn depth_tracks_send_and_recv() { + let (tx, mut rx) = new_telemetry_buffer(16); + + tx.send_trace(vec![1]); + tx.send_trace(vec![2]); + tx.send_trace(vec![3]); + assert_eq!(tx.metrics().depth(), 3); + + rx.recv().await.unwrap(); + assert_eq!(rx.metrics().depth(), 2); + + let remaining = rx.drain(); + assert_eq!(remaining.len(), 2); + assert_eq!(rx.metrics().depth(), 0); + } + + #[tokio::test] + async fn drop_count_increments_on_each_overflow() { + let (tx, _rx) = new_telemetry_buffer(2); + + tx.send_trace(vec![1]); + tx.send_trace(vec![2]); + assert_eq!(tx.metrics().drops(), 0); + + for _ in 0..5 { + tx.send_trace(vec![99]); + } + assert_eq!(tx.metrics().drops(), 5); + assert_eq!(tx.metrics().depth(), 2); + } + + #[tokio::test] + async fn metrics_shared_across_clones() { + let (tx, mut rx) = new_telemetry_buffer(16); + let tx2 = tx.clone(); + + tx.send_trace(vec![1]); + tx2.send_trace(vec![2]); + tx.send_ocsf(vec![3]); + + assert_eq!(tx.metrics().depth(), 3); + assert_eq!(tx2.metrics().depth(), 3); + + rx.recv().await.unwrap(); + assert_eq!(tx.metrics().depth(), 2); + assert_eq!(tx2.metrics().depth(), 2); + } + + #[tokio::test] + async fn recv_returns_none_when_all_senders_dropped() { + let (tx, mut rx) = new_telemetry_buffer(16); + tx.send_trace(vec![1]); + drop(tx); + + assert!(rx.recv().await.is_some()); + assert!(rx.recv().await.is_none()); + } } diff --git a/crates/openshell-supervisor-network/src/otlp/enrichment.rs b/crates/openshell-supervisor-network/src/otlp/enrichment.rs index 4078028f0e..03c0de69ec 100644 --- a/crates/openshell-supervisor-network/src/otlp/enrichment.rs +++ b/crates/openshell-supervisor-network/src/otlp/enrichment.rs @@ -161,4 +161,148 @@ mod tests { assert_eq!(resource.attributes.len(), 1); assert_eq!(resource.attributes[0].key, "openshell.telemetry.source"); } + + #[test] + fn enrichment_strips_agent_supplied_trusted_keys() { + use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value}; + use opentelemetry_proto::tonic::resource::v1::Resource; + + let req = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![ + KeyValue { + key: "openshell.sandbox.id".into(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue( + "agent-spoofed-id".into(), + )), + }), + key_strindex: 0, + }, + KeyValue { + key: "openshell.telemetry.source".into(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue("fake".into())), + }), + key_strindex: 0, + }, + ], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_spans: vec![], + schema_url: String::new(), + }], + }; + let raw = req.encode_to_vec(); + + let result = enrich_spans(&raw, ContentType::Protobuf, &test_metadata(), true).unwrap(); + let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); + let attrs = &decoded.resource_spans[0] + .resource + .as_ref() + .unwrap() + .attributes; + + let sandbox_ids: Vec<_> = attrs + .iter() + .filter(|a| a.key == "openshell.sandbox.id") + .collect(); + assert_eq!(sandbox_ids.len(), 1, "should have exactly one sandbox.id"); + + let sources: Vec<_> = attrs + .iter() + .filter(|a| a.key == "openshell.telemetry.source") + .collect(); + assert_eq!(sources.len(), 1, "should have exactly one telemetry.source"); + + if let Some(AnyValue { + value: Some(any_value::Value::StringValue(v)), + }) = &sandbox_ids[0].value + { + assert_eq!(v, "sb-123", "should use supervisor's value, not agent's"); + } + } + + #[test] + fn enrichment_preserves_non_trusted_agent_attributes() { + use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value}; + use opentelemetry_proto::tonic::resource::v1::Resource; + + let req = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![KeyValue { + key: "my.custom.attr".into(), + value: Some(AnyValue { + value: Some(any_value::Value::StringValue("keep-me".into())), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: Vec::new(), + }), + scope_spans: vec![], + schema_url: String::new(), + }], + }; + let raw = req.encode_to_vec(); + + let result = enrich_spans(&raw, ContentType::Protobuf, &test_metadata(), true).unwrap(); + let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); + let attrs = &decoded.resource_spans[0] + .resource + .as_ref() + .unwrap() + .attributes; + + assert!( + attrs.iter().any(|a| a.key == "my.custom.attr"), + "custom agent attribute should be preserved" + ); + assert!( + attrs.iter().any(|a| a.key == "openshell.sandbox.id"), + "enrichment attributes should also be present" + ); + } + + #[test] + fn enrichment_handles_json_content_type() { + let req = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: None, + scope_spans: vec![ScopeSpans { + scope: None, + spans: vec![Span { + name: "json-span".into(), + ..Default::default() + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + }; + let json = serde_json::to_vec(&req).unwrap(); + + let result = + enrich_spans(&json, ContentType::Json, &test_metadata(), true).unwrap(); + + let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); + let resource = decoded.resource_spans[0].resource.as_ref().unwrap(); + assert!(resource + .attributes + .iter() + .any(|a| a.key == "openshell.telemetry.source")); + } + + #[test] + fn enrichment_rejects_invalid_protobuf() { + let garbage = vec![0xFF, 0xFE, 0xFD, 0xFC]; + let result = enrich_spans(&garbage, ContentType::Protobuf, &test_metadata(), true); + assert!( + matches!(result, Err(EnrichmentError::ProtobufDecode(_))), + "should return ProtobufDecode error" + ); + } } diff --git a/crates/openshell-supervisor-network/src/otlp/mod.rs b/crates/openshell-supervisor-network/src/otlp/mod.rs index bb83581668..21b8212be2 100644 --- a/crates/openshell-supervisor-network/src/otlp/mod.rs +++ b/crates/openshell-supervisor-network/src/otlp/mod.rs @@ -297,3 +297,107 @@ fn spawn_forwarder( } }) } + +#[cfg(test)] +mod tests { + use super::*; + use openshell_ocsf::OcsfRelaySink; + + #[test] + fn rate_limiter_acquires_initial_tokens() { + let (buf_tx, _rx) = buffer::new_telemetry_buffer(64); + let sink = RateLimitedOcsfSink::new(buf_tx, 10); + + for i in 0..10 { + assert!(sink.try_acquire(), "token {i} should be available"); + } + assert!(!sink.try_acquire(), "11th token should fail"); + } + + #[test] + fn rate_limiter_drops_when_exhausted() { + let (buf_tx, mut rx) = buffer::new_telemetry_buffer(64); + let sink = RateLimitedOcsfSink::new(buf_tx, 2); + + sink.send(vec![1]); + sink.send(vec![2]); + sink.send(vec![3]); + + assert_eq!(sink.drops(), 1); + let items = rx.drain(); + assert_eq!(items.len(), 2); + } + + #[test] + fn rate_limiter_refills_after_time() { + let (buf_tx, _rx) = buffer::new_telemetry_buffer(64); + let sink = RateLimitedOcsfSink::new(buf_tx, 100); + + for _ in 0..100 { + sink.try_acquire(); + } + assert!(!sink.try_acquire(), "should be exhausted"); + + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!(sink.try_acquire(), "should have refilled after 50ms"); + } + + #[tokio::test] + async fn forwarder_constructs_telemetry_data_messages() { + let (buf_tx, buf_rx) = buffer::new_telemetry_buffer(64); + let (session_tx, mut session_rx) = mpsc::channel::(64); + let drop_counter = Arc::new(AtomicU64::new(0)); + + buf_tx.send_trace(vec![1, 2, 3]); + buf_tx.send_ocsf(vec![4, 5, 6]); + drop(buf_tx); + + let handle = spawn_forwarder(buf_rx, session_tx, "sb-test".into(), drop_counter); + + let msg1 = session_rx.recv().await.unwrap(); + if let Some(supervisor_message::Payload::Telemetry(tel)) = msg1.payload { + assert_eq!(tel.sandbox_id, "sb-test"); + assert_eq!(tel.trace_data, vec![1, 2, 3]); + assert!(tel.ocsf_events.is_empty()); + } else { + panic!("expected Telemetry payload for trace"); + } + + let msg2 = session_rx.recv().await.unwrap(); + if let Some(supervisor_message::Payload::Telemetry(tel)) = msg2.payload { + assert_eq!(tel.sandbox_id, "sb-test"); + assert!(tel.trace_data.is_empty()); + assert_eq!(tel.ocsf_events, vec![vec![4, 5, 6]]); + } else { + panic!("expected Telemetry payload for OCSF"); + } + + handle.await.unwrap(); + } + + #[tokio::test] + async fn forwarder_increments_session_drop_counter() { + let (buf_tx, buf_rx) = buffer::new_telemetry_buffer(64); + let (session_tx, _session_rx) = mpsc::channel::(1); + let drop_counter = Arc::new(AtomicU64::new(0)); + + // Fill the session channel + session_tx + .send(SupervisorMessage { payload: None }) + .await + .unwrap(); + + buf_tx.send_trace(vec![1]); + buf_tx.send_trace(vec![2]); + buf_tx.send_trace(vec![3]); + drop(buf_tx); + + let handle = spawn_forwarder(buf_rx, session_tx, "sb-test".into(), drop_counter.clone()); + handle.await.unwrap(); + + assert!( + drop_counter.load(Ordering::Relaxed) >= 2, + "should have dropped at least 2 messages (channel capacity 1, pre-filled)" + ); + } +} From 48e9ece979889aa49b1df146c3d0d08e7118496a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 15:25:52 +0200 Subject: [PATCH 7/8] feat(observability): add OTLP relay capability negotiation logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OTLP relay was silently disabled when the gateway config lacked an [openshell.gateway.otlp] section, with no log output at any decision point. This made it impossible to diagnose why telemetry data was not flowing from supervisor to gateway. Add info-level logging when the relay exporter connects and when capabilities are confirmed, and debug-level logging for the normal inactive paths. Add a commented-out OTLP section to the Docker gateway config as a setup reference. Fix pre-existing rustfmt and clippy issues in OTLP relay code. Signed-off-by: Roland Huß --- crates/openshell-core/src/sandbox_env.rs | 10 +++++---- .../src/supervisor_session.rs | 21 +++++++++++++------ .../openshell-server/src/telemetry_relay.rs | 13 +++++++++--- .../src/otlp/enrichment.rs | 13 ++++++------ .../src/process.rs | 5 ++++- .../openshell-supervisor-process/src/ssh.rs | 5 ++++- .../src/supervisor_session.rs | 10 ++++++++- deploy/docker/gateway.toml | 7 +++++++ 8 files changed, 62 insertions(+), 22 deletions(-) diff --git a/crates/openshell-core/src/sandbox_env.rs b/crates/openshell-core/src/sandbox_env.rs index 0f12501323..d3fa9680d8 100644 --- a/crates/openshell-core/src/sandbox_env.rs +++ b/crates/openshell-core/src/sandbox_env.rs @@ -247,10 +247,12 @@ pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; /// Set to `http/protobuf` when the telemetry relay is active. pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; -/// Default OTLP receiver bind address and port. All current topologies keep the -/// process supervisor co-located with the agent, so localhost is correct. Future -/// topologies that move the supervisor out of the agent's network namespace -/// would derive the address from the topology (e.g., pod IP via downward API). +/// Default OTLP receiver bind address and port. +/// +/// All current topologies keep the process supervisor co-located with the +/// agent, so localhost is correct. Future topologies that move the supervisor +/// out of the agent's network namespace would derive the address from the +/// topology (e.g., pod IP via downward API). pub const OTLP_RECEIVER_ADDR: &str = "127.0.0.1:4318"; /// Default OTLP receiver endpoint URL for agent env var injection. diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index 5754b19aa6..a77b87d05b 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -1041,9 +1041,21 @@ fn confirm_capabilities(advertised: &[String], state: &Arc) -> Vec< "telemetry_relay" if state.telemetry_relay_exporter.is_some() => { confirmed.push(cap.clone()); } - _ => {} + "telemetry_relay" => { + debug!( + capability = "telemetry_relay", + "supervisor advertised telemetry_relay but gateway has no \ + [openshell.gateway.otlp] config; capability not confirmed" + ); + } + other => { + debug!(capability = %other, "ignoring unknown supervisor capability"); + } } } + if !confirmed.is_empty() { + info!(capabilities = ?confirmed, "confirmed supervisor capabilities"); + } confirmed } @@ -1062,11 +1074,8 @@ fn handle_telemetry_data( let trace_data = telemetry.trace_data; let sandbox_id = sandbox_id.to_string(); tokio::spawn(async move { - match tokio::time::timeout( - Duration::from_secs(10), - exporter.export_raw(trace_data), - ) - .await + match tokio::time::timeout(Duration::from_secs(10), exporter.export_raw(trace_data)) + .await { Ok(Err(e)) => { debug!( diff --git a/crates/openshell-server/src/telemetry_relay.rs b/crates/openshell-server/src/telemetry_relay.rs index 7701ce9e08..c8fb43eb70 100644 --- a/crates/openshell-server/src/telemetry_relay.rs +++ b/crates/openshell-server/src/telemetry_relay.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; use prost::Message; use tonic::transport::Channel; -use tracing::debug; +use tracing::{debug, info}; use opentelemetry_proto::tonic::collector::trace::v1::trace_service_client::TraceServiceClient; @@ -70,10 +70,17 @@ pub enum ConnectError { pub async fn try_create_exporter( config_file: Option<&crate::config_file::ConfigFile>, ) -> Option> { - let otlp = config_file?.openshell.gateway.otlp.as_ref()?; + let Some(cf) = config_file else { + debug!("no config file; telemetry relay disabled"); + return None; + }; + let Some(otlp) = cf.openshell.gateway.otlp.as_ref() else { + debug!("no [openshell.gateway.otlp] section in config; telemetry relay disabled"); + return None; + }; match TelemetryRelayExporter::connect(&otlp.endpoint).await { Ok(exporter) => { - debug!(endpoint = %otlp.endpoint, "telemetry relay exporter connected"); + info!(endpoint = %otlp.endpoint, "telemetry relay exporter connected"); Some(Arc::new(exporter)) } Err(e) => { diff --git a/crates/openshell-supervisor-network/src/otlp/enrichment.rs b/crates/openshell-supervisor-network/src/otlp/enrichment.rs index 03c0de69ec..cff6113c32 100644 --- a/crates/openshell-supervisor-network/src/otlp/enrichment.rs +++ b/crates/openshell-supervisor-network/src/otlp/enrichment.rs @@ -285,15 +285,16 @@ mod tests { }; let json = serde_json::to_vec(&req).unwrap(); - let result = - enrich_spans(&json, ContentType::Json, &test_metadata(), true).unwrap(); + let result = enrich_spans(&json, ContentType::Json, &test_metadata(), true).unwrap(); let decoded = ExportTraceServiceRequest::decode(result.as_slice()).unwrap(); let resource = decoded.resource_spans[0].resource.as_ref().unwrap(); - assert!(resource - .attributes - .iter() - .any(|a| a.key == "openshell.telemetry.source")); + assert!( + resource + .attributes + .iter() + .any(|a| a.key == "openshell.telemetry.source") + ); } #[test] diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 3f1b07cc52..3a23718c71 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -832,7 +832,10 @@ impl ProcessHandle { // Set OTEL env vars so agent SDKs export to the supervisor's OTLP receiver. // The relay binds on 127.0.0.1:4318 inside the agent's network namespace // (all current topologies keep the process supervisor co-located with the agent). - for (key, value) in child_env::otel_env_vars(openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, "http/protobuf") { + for (key, value) in child_env::otel_env_vars( + openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, + "http/protobuf", + ) { cmd.env(key, value); } diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 2bc76a7a3e..49fddc1143 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -1222,7 +1222,10 @@ fn apply_child_env( for (key, value) in child_env::proxy_env_vars(url) { cmd.env(key, value); } - for (key, value) in child_env::otel_env_vars(openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, "http/protobuf") { + for (key, value) in child_env::otel_env_vars( + openshell_core::sandbox_env::OTLP_RECEIVER_ENDPOINT, + "http/protobuf", + ) { cmd.env(key, value); } } diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 6cbc8442fd..f2906a83a9 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -30,7 +30,7 @@ use openshell_ocsf::{ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::sync::mpsc; use tokio_stream::StreamExt; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use openshell_core::grpc_client; use openshell_core::net::set_tcp_nodelay_best_effort; @@ -395,6 +395,14 @@ async fn run_single_session( }; let telemetry_confirmed = accepted.capabilities.iter().any(|c| c == "telemetry_relay"); + if telemetry_confirmed { + info!("gateway confirmed telemetry_relay capability; OTLP drain active"); + } else { + debug!( + "gateway did not confirm telemetry_relay; \ + OTLP forwarding disabled for this session" + ); + } let heartbeat_secs = accepted.heartbeat_interval_secs.max(5); let event = session_established_event( diff --git a/deploy/docker/gateway.toml b/deploy/docker/gateway.toml index 4fe84d633a..0e31171625 100644 --- a/deploy/docker/gateway.toml +++ b/deploy/docker/gateway.toml @@ -49,3 +49,10 @@ sandbox_namespace = "openshell" # The gateway must be published on port 8080 on the Docker host so that # host.openshell.internal:8080 resolves to the gateway container. grpc_endpoint = "http://host.openshell.internal:8080" + +# Uncomment to enable the OTLP telemetry relay. The supervisor collects +# traces from sandbox agents and forwards them through the gateway to the +# configured collector endpoint (gRPC, typically port 4317). +# [openshell.gateway.otlp] +# endpoint = "http://host.docker.internal:4317" +# service_name = "openshell-gateway-dev" From 250fee22cd51f945e355158f826464678ff00e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Sat, 5 Sep 2026 17:15:59 +0200 Subject: [PATCH 8/8] refactor(observability): rename TelemetryData to OtelExportData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the proto message and all related types to clarify that this carries OpenTelemetry signal data destined for an OTLP collector, not product analytics telemetry. Add a `oneof signal` wrapper around `trace_data` so future metrics and logs signals can be added without breaking the wire format. Renames: - Proto: TelemetryData -> OtelExportData, oneof payload field telemetry -> otel_export - Capability: "telemetry_relay" -> "otel_export" - Gateway: TelemetryRelayExporter -> OtelRelayExporter, telemetry_relay.rs -> otel_relay.rs - Supervisor: TelemetryRelay -> OtelRelay - All related variables, functions, and log messages Signed-off-by: Roland Huß --- architecture/sandbox.md | 12 +-- crates/openshell-sandbox/src/lib.rs | 34 ++++---- crates/openshell-server/src/lib.rs | 10 +-- .../src/{telemetry_relay.rs => otel_relay.rs} | 16 ++-- .../src/supervisor_session.rs | 78 ++++++++++--------- .../src/otlp/mod.rs | 69 ++++++++-------- .../openshell-supervisor-process/src/run.rs | 4 +- .../src/supervisor_session.rs | 36 ++++----- proto/openshell.proto | 22 +++--- 9 files changed, 147 insertions(+), 134 deletions(-) rename crates/openshell-server/src/{telemetry_relay.rs => otel_relay.rs} (84%) diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 65bd1b1b74..0a3c0dfabb 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -475,7 +475,7 @@ collector without requiring direct egress from the sandbox. Agent process --> OTLP HTTP (127.0.0.1:4318) --> Supervisor receiver --> Enrichment (sandbox resource attributes) --> Bounded buffer (4096 slots, shared traces + OCSF) - --> Forwarder --> Session channel (TelemetryData message) + --> Forwarder --> Session channel (OtelExportData message) --> Gateway --> Dedicated SpanExporter --> External OTLP collector ``` @@ -483,7 +483,7 @@ Agent process --> OTLP HTTP (127.0.0.1:4318) --> Supervisor receiver The OTLP HTTP receiver binds to `127.0.0.1:4318` only when the relay is active (the gateway has `[openshell.gateway.otlp]` configured and confirms -the `telemetry_relay` capability). When OTLP is not configured, no port is +the `otel_export` capability). When OTLP is not configured, no port is bound and no receiver runs. The bind address depends on the supervisor topology. In all current @@ -513,9 +513,9 @@ pass-through forwarding. ### Activation and Capability Negotiation The relay is opt-in. It starts only when the gateway has `[openshell.gateway.otlp]` -configured. The supervisor advertises `"telemetry_relay"` in +configured. The supervisor advertises `"otel_export"` in `SupervisorHello.capabilities`. The gateway confirms via `SessionAccepted.capabilities`. -The supervisor gates `TelemetryData` sending on this confirmation. When the +The supervisor gates `OtelExportData` sending on this confirmation. When the relay is active, the supervisor sets `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_PROTOCOL` in agent child processes via `child_env.rs`. @@ -534,8 +534,8 @@ controls the OCSF event rate, with configurable rate and drop counter. ### Gateway-Side Handling -The gateway receives `TelemetryData` messages and exports trace data through a -dedicated `TelemetryRelayExporter` that connects directly to the configured OTLP +The gateway receives `OtelExportData` messages and exports trace data through a +dedicated `OtelRelayExporter` that connects directly to the configured OTLP collector. This bypasses the gateway's own `SdkTracerProvider` to preserve the supervisor-enriched resource attributes. OCSF events are emitted via `tracing::info!` on the `ocsf_relay` target. diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 77e95b281b..a7d53aa793 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -853,7 +853,7 @@ pub async fn run_sandbox( tokio::pin!(proxy_exited); #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] - let mut telemetry_relay_handle: Option = None; + let mut otel_relay_handle: Option = None; let exit_code = if process_enabled { let ca_file_paths = networking @@ -955,18 +955,18 @@ pub async fn run_sandbox( None }; - // Telemetry relay: bind OTLP receiver for all Linux topologies. + // OTEL relay: bind OTLP receiver for all Linux topologies. // All current topologies keep the process supervisor co-located with // the agent, so 127.0.0.1 is reachable from agent processes. Future // topologies that move the supervisor out of the workload pod would // need to derive the bind address from the topology (e.g., pod IP // via downward API) and update OTEL_EXPORTER_OTLP_ENDPOINT to match. - let telemetry_rx = { + let otel_rx = { #[cfg(target_os = "linux")] { let otlp_addr = openshell_core::sandbox_env::OTLP_RECEIVER_ADDR; - let (telemetry_session_tx, telemetry_session_rx) = + let (otel_session_tx, otel_session_rx) = tokio::sync::mpsc::channel::(64); let relay_config = openshell_supervisor_network::otlp::RelayConfig::default(); @@ -981,10 +981,10 @@ pub async fn run_sandbox( driver: std::env::var(openshell_core::sandbox_env::SUPERVISOR_TOPOLOGY) .unwrap_or_else(|_| "container".to_string()), }; - let relay = openshell_supervisor_network::otlp::TelemetryRelay::new( + let relay = openshell_supervisor_network::otlp::OtelRelay::new( relay_config, metadata, - telemetry_session_tx, + otel_session_tx, ); let bind_addr: std::net::SocketAddr = otlp_addr.parse().unwrap(); @@ -993,29 +993,29 @@ pub async fn run_sandbox( match ns.bind_tcp_in_netns(otlp_addr).await { Ok(listener) => { let handle = relay.start_with_listener(listener); - tracing::info!(bind = %bind_addr, "telemetry relay started (netns)"); - telemetry_relay_handle = Some(handle); + tracing::info!(bind = %bind_addr, "OTEL relay started (netns)"); + otel_relay_handle = Some(handle); } Err(e) => { - tracing::warn!(error = %e, "telemetry relay failed to bind in netns; continuing without relay"); + tracing::warn!(error = %e, "OTEL relay failed to bind in netns; continuing without relay"); } } } else { match relay.start(bind_addr).await { Ok(handle) => { - tracing::info!(bind = %bind_addr, "telemetry relay started"); - telemetry_relay_handle = Some(handle); + tracing::info!(bind = %bind_addr, "OTEL relay started"); + otel_relay_handle = Some(handle); } Err(e) => { - tracing::warn!(error = %e, "telemetry relay failed to start; continuing without relay"); + tracing::warn!(error = %e, "OTEL relay failed to start; continuing without relay"); } } } - Some(telemetry_session_rx) + Some(otel_session_rx) } #[cfg(not(target_os = "linux"))] { - debug!("telemetry relay not available on this platform"); + debug!("OTEL relay not available on this platform"); None } }; @@ -1048,7 +1048,7 @@ pub async fn run_sandbox( bypass_denial_tx, #[cfg(target_os = "linux")] bypass_activity_tx, - telemetry_rx, + otel_rx, ); if let Some(control_closed) = process_control_closed.as_mut() { @@ -1208,9 +1208,9 @@ pub async fn run_sandbox( } }; - // Drain telemetry relay before tearing down networking so short-lived + // Drain OTEL relay before tearing down networking so short-lived // agents don't lose their final spans. - if let Some(handle) = telemetry_relay_handle { + if let Some(handle) = otel_relay_handle { handle.shutdown().await; } diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 881351b83b..227fed99b2 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -38,7 +38,7 @@ mod service_routing; mod ssh_sessions; pub mod supervisor_session; mod telemetry; -pub(crate) mod telemetry_relay; +pub(crate) mod otel_relay; #[cfg(any(test, feature = "test-support"))] pub mod test_support; mod tls; @@ -345,7 +345,7 @@ pub struct ServerState { /// Dedicated OTLP exporter for relayed telemetry from supervisors. /// `None` when the gateway has no OTLP endpoint configured. - pub telemetry_relay_exporter: Option>, + pub otel_relay_exporter: Option>, } fn is_benign_tls_handshake_failure(error: &std::io::Error) -> bool { @@ -433,7 +433,7 @@ impl ServerState { provider_profile_sources: provider_profile_sources::ProviderProfileSources::with_default_sources(), admin_role, - telemetry_relay_exporter: None, + otel_relay_exporter: None, } } } @@ -667,8 +667,8 @@ pub(crate) async fn run_server( state.middleware_registry = middleware_registry; state.gateway_interceptors = gateway_interceptors; state.provider_profile_sources = provider_profile_sources; - state.telemetry_relay_exporter = - telemetry_relay::try_create_exporter(config_file.as_ref()).await; + state.otel_relay_exporter = + otel_relay::try_create_exporter(config_file.as_ref()).await; state.sandbox_jwt_issuer = sandbox_jwt_issuer.clone(); state.sandbox_jwt_authenticator = sandbox_jwt_authenticator; if let Some(issuer) = sandbox_jwt_issuer { diff --git a/crates/openshell-server/src/telemetry_relay.rs b/crates/openshell-server/src/otel_relay.rs similarity index 84% rename from crates/openshell-server/src/telemetry_relay.rs rename to crates/openshell-server/src/otel_relay.rs index c8fb43eb70..a87cb63b79 100644 --- a/crates/openshell-server/src/telemetry_relay.rs +++ b/crates/openshell-server/src/otel_relay.rs @@ -18,11 +18,11 @@ use opentelemetry_proto::tonic::collector::trace::v1::trace_service_client::Trac /// Exporter that forwards raw protobuf-encoded trace data to an OTLP collector. #[derive(Debug, Clone)] -pub struct TelemetryRelayExporter { +pub struct OtelRelayExporter { client: TraceServiceClient, } -impl TelemetryRelayExporter { +impl OtelRelayExporter { /// Connect to the OTLP collector at the given gRPC endpoint. pub async fn connect(endpoint: &str) -> Result { let channel = Channel::from_shared(endpoint.to_string()) @@ -69,25 +69,25 @@ pub enum ConnectError { /// Create a relay exporter from the gateway's OTLP config, if configured. pub async fn try_create_exporter( config_file: Option<&crate::config_file::ConfigFile>, -) -> Option> { +) -> Option> { let Some(cf) = config_file else { - debug!("no config file; telemetry relay disabled"); + debug!("no config file; OTEL relay disabled"); return None; }; let Some(otlp) = cf.openshell.gateway.otlp.as_ref() else { - debug!("no [openshell.gateway.otlp] section in config; telemetry relay disabled"); + debug!("no [openshell.gateway.otlp] section in config; OTEL relay disabled"); return None; }; - match TelemetryRelayExporter::connect(&otlp.endpoint).await { + match OtelRelayExporter::connect(&otlp.endpoint).await { Ok(exporter) => { - info!(endpoint = %otlp.endpoint, "telemetry relay exporter connected"); + info!(endpoint = %otlp.endpoint, "OTEL relay exporter connected"); Some(Arc::new(exporter)) } Err(e) => { tracing::warn!( endpoint = %otlp.endpoint, error = %e, - "failed to connect telemetry relay exporter; relay disabled" + "failed to connect OTEL relay exporter; relay disabled" ); None } diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index a77b87d05b..99e7371ac7 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -1020,8 +1020,8 @@ fn handle_supervisor_message( "supervisor session: relay closed by supervisor" ); } - Some(supervisor_message::Payload::Telemetry(telemetry)) => { - handle_telemetry_data(state, sandbox_id, session_id, telemetry); + Some(supervisor_message::Payload::OtelExport(otel_data)) => { + handle_otel_export(state, sandbox_id, session_id, otel_data); } _ => { debug!( @@ -1038,13 +1038,13 @@ fn confirm_capabilities(advertised: &[String], state: &Arc) -> Vec< let mut confirmed = Vec::new(); for cap in advertised { match cap.as_str() { - "telemetry_relay" if state.telemetry_relay_exporter.is_some() => { + "otel_export" if state.otel_relay_exporter.is_some() => { confirmed.push(cap.clone()); } - "telemetry_relay" => { + "otel_export" => { debug!( - capability = "telemetry_relay", - "supervisor advertised telemetry_relay but gateway has no \ + capability = "otel_export", + "supervisor advertised otel_export but gateway has no \ [openshell.gateway.otlp] config; capability not confirmed" ); } @@ -1059,49 +1059,55 @@ fn confirm_capabilities(advertised: &[String], state: &Arc) -> Vec< confirmed } -/// Handle incoming telemetry data from the supervisor: forward trace data to +/// Handle incoming OTEL export data from the supervisor: forward trace data to /// the configured OTLP collector and dispatch OCSF events to the log sink. -fn handle_telemetry_data( +fn handle_otel_export( state: &Arc, sandbox_id: &str, session_id: &str, - telemetry: openshell_core::proto::TelemetryData, + otel_data: openshell_core::proto::OtelExportData, ) { - if !telemetry.trace_data.is_empty() - && let Some(relay_exporter) = state.telemetry_relay_exporter.as_ref() + if let Some(openshell_core::proto::otel_export_data::Signal::TraceData(trace_data)) = + otel_data.signal { - let exporter = relay_exporter.clone(); - let trace_data = telemetry.trace_data; - let sandbox_id = sandbox_id.to_string(); - tokio::spawn(async move { - match tokio::time::timeout(Duration::from_secs(10), exporter.export_raw(trace_data)) - .await - { - Ok(Err(e)) => { - debug!( - sandbox_id = %sandbox_id, - error = %e, - "telemetry relay: failed to export trace data" - ); - } - Err(_) => { - debug!( - sandbox_id = %sandbox_id, - "telemetry relay: export timed out" - ); - } - Ok(Ok(())) => {} + if !trace_data.is_empty() { + if let Some(relay_exporter) = state.otel_relay_exporter.as_ref() { + let exporter = relay_exporter.clone(); + let sandbox_id = sandbox_id.to_string(); + tokio::spawn(async move { + match tokio::time::timeout( + Duration::from_secs(10), + exporter.export_raw(trace_data), + ) + .await + { + Ok(Err(e)) => { + debug!( + sandbox_id = %sandbox_id, + error = %e, + "OTEL relay: failed to export trace data" + ); + } + Err(_) => { + debug!( + sandbox_id = %sandbox_id, + "OTEL relay: export timed out" + ); + } + Ok(Ok(())) => {} + } + }); } - }); + } } const MAX_OCSF_EVENT_SIZE: usize = 256 * 1024; - for ocsf_event in &telemetry.ocsf_events { + for ocsf_event in &otel_data.ocsf_events { if ocsf_event.len() > MAX_OCSF_EVENT_SIZE { debug!( sandbox_id = %sandbox_id, size = ocsf_event.len(), - "telemetry relay: OCSF event too large, skipping" + "OTEL relay: OCSF event too large, skipping" ); continue; } @@ -1109,7 +1115,7 @@ fn handle_telemetry_data( if serde_json::from_str::(json_str).is_err() { debug!( sandbox_id = %sandbox_id, - "telemetry relay: OCSF event is not valid JSON, skipping" + "OTEL relay: OCSF event is not valid JSON, skipping" ); continue; } diff --git a/crates/openshell-supervisor-network/src/otlp/mod.rs b/crates/openshell-supervisor-network/src/otlp/mod.rs index 21b8212be2..b164eb7192 100644 --- a/crates/openshell-supervisor-network/src/otlp/mod.rs +++ b/crates/openshell-supervisor-network/src/otlp/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! OTLP telemetry relay for the sandbox supervisor. +//! OTLP relay for the sandbox supervisor. //! //! Receives OTLP trace data from agent processes over HTTP, enriches spans //! with sandbox resource attributes, buffers them in a bounded channel, and @@ -19,13 +19,13 @@ use tokio::sync::mpsc; use tracing::info; use openshell_core::proto::SupervisorMessage; -use openshell_core::proto::TelemetryData; +use openshell_core::proto::{OtelExportData, otel_export_data}; use openshell_core::proto::supervisor_message; use buffer::{TelemetryReceiver, TelemetrySender}; /// Rate-limited OCSF relay sink that implements token bucket rate limiting -/// and sends accepted events through the telemetry buffer as OCSF bytes. +/// and sends accepted events through the OTEL buffer as OCSF bytes. pub struct RateLimitedOcsfSink { buf_tx: TelemetrySender, tokens: std::sync::atomic::AtomicU32, @@ -98,7 +98,7 @@ impl openshell_ocsf::OcsfRelaySink for RateLimitedOcsfSink { } } -/// Configuration for the telemetry relay. +/// Configuration for the OTEL relay. #[derive(Debug, Clone)] pub struct RelayConfig { pub enabled: bool, @@ -129,42 +129,42 @@ pub struct SandboxMetadata { pub driver: String, } -/// Handle returned by [`TelemetryRelay::start`] for lifecycle management. +/// Handle returned by [`OtelRelay::start`] for lifecycle management. pub struct RelayHandle { shutdown_tx: tokio::sync::oneshot::Sender<()>, forwarder_handle: tokio::task::JoinHandle<()>, receiver_handle: tokio::task::JoinHandle<()>, session_drop_counter: Arc, - pub telemetry_tx: TelemetrySender, + pub otel_tx: TelemetrySender, } impl RelayHandle { /// Gracefully shut down the relay: stop the HTTP receiver, then drain - /// remaining buffered telemetry through the forwarder. + /// remaining buffered data through the forwarder. pub async fn shutdown(self) { - let metrics = self.telemetry_tx.metrics().clone(); + let metrics = self.otel_tx.metrics().clone(); let _ = self.shutdown_tx.send(()); let _ = self.receiver_handle.await; - drop(self.telemetry_tx); + drop(self.otel_tx); let _ = self.forwarder_handle.await; info!( buffer_drops = metrics.drops(), queue_depth = metrics.depth(), session_drops = self.session_drop_counter.load(Ordering::Relaxed), - "telemetry relay shut down" + "OTEL relay shut down" ); } } -/// The telemetry relay manages receive, enrich, buffer, and forward. -pub struct TelemetryRelay { +/// The OTEL relay manages receive, enrich, buffer, and forward. +pub struct OtelRelay { config: RelayConfig, metadata: SandboxMetadata, session_tx: mpsc::Sender, sandbox_id: String, } -impl TelemetryRelay { +impl OtelRelay { pub fn new( config: RelayConfig, metadata: SandboxMetadata, @@ -203,7 +203,7 @@ impl TelemetryRelay { info!( buffer_capacity = self.config.buffer_capacity, enrichment = self.config.enrichment_enabled, - "telemetry relay started (pre-bound listener)" + "OTEL relay started (pre-bound listener)" ); RelayHandle { @@ -211,7 +211,7 @@ impl TelemetryRelay { forwarder_handle, receiver_handle, session_drop_counter, - telemetry_tx: buf_tx, + otel_tx: buf_tx, } } @@ -244,7 +244,7 @@ impl TelemetryRelay { bind = %bind_addr, buffer_capacity = self.config.buffer_capacity, enrichment = self.config.enrichment_enabled, - "telemetry relay started" + "OTEL relay started" ); Ok(RelayHandle { @@ -252,7 +252,7 @@ impl TelemetryRelay { forwarder_handle, receiver_handle, session_drop_counter, - telemetry_tx: buf_tx, + otel_tx: buf_tx, }) } } @@ -264,7 +264,7 @@ pub enum StartError { Bind(std::io::Error), } -/// Spawn the forwarder task that drains the buffer and sends `TelemetryData` +/// Spawn the forwarder task that drains the buffer and sends `OtelExportData` /// via the session channel using `try_send` (non-blocking). fn spawn_forwarder( mut buf_rx: TelemetryReceiver, @@ -276,16 +276,16 @@ fn spawn_forwarder( while let Some(item) = buf_rx.recv().await { let msg = match item { buffer::TelemetryItem::Trace(data) => SupervisorMessage { - payload: Some(supervisor_message::Payload::Telemetry(TelemetryData { + payload: Some(supervisor_message::Payload::OtelExport(OtelExportData { sandbox_id: sandbox_id.clone(), - trace_data: data, + signal: Some(otel_export_data::Signal::TraceData(data)), ocsf_events: Vec::new(), })), }, buffer::TelemetryItem::Ocsf(data) => SupervisorMessage { - payload: Some(supervisor_message::Payload::Telemetry(TelemetryData { + payload: Some(supervisor_message::Payload::OtelExport(OtelExportData { sandbox_id: sandbox_id.clone(), - trace_data: Vec::new(), + signal: None, ocsf_events: vec![data], })), }, @@ -343,7 +343,7 @@ mod tests { } #[tokio::test] - async fn forwarder_constructs_telemetry_data_messages() { + async fn forwarder_constructs_otel_export_messages() { let (buf_tx, buf_rx) = buffer::new_telemetry_buffer(64); let (session_tx, mut session_rx) = mpsc::channel::(64); let drop_counter = Arc::new(AtomicU64::new(0)); @@ -355,21 +355,24 @@ mod tests { let handle = spawn_forwarder(buf_rx, session_tx, "sb-test".into(), drop_counter); let msg1 = session_rx.recv().await.unwrap(); - if let Some(supervisor_message::Payload::Telemetry(tel)) = msg1.payload { - assert_eq!(tel.sandbox_id, "sb-test"); - assert_eq!(tel.trace_data, vec![1, 2, 3]); - assert!(tel.ocsf_events.is_empty()); + if let Some(supervisor_message::Payload::OtelExport(otel)) = msg1.payload { + assert_eq!(otel.sandbox_id, "sb-test"); + assert_eq!( + otel.signal, + Some(otel_export_data::Signal::TraceData(vec![1, 2, 3])) + ); + assert!(otel.ocsf_events.is_empty()); } else { - panic!("expected Telemetry payload for trace"); + panic!("expected OtelExport payload for trace"); } let msg2 = session_rx.recv().await.unwrap(); - if let Some(supervisor_message::Payload::Telemetry(tel)) = msg2.payload { - assert_eq!(tel.sandbox_id, "sb-test"); - assert!(tel.trace_data.is_empty()); - assert_eq!(tel.ocsf_events, vec![vec![4, 5, 6]]); + if let Some(supervisor_message::Payload::OtelExport(otel)) = msg2.payload { + assert_eq!(otel.sandbox_id, "sb-test"); + assert_eq!(otel.signal, None); + assert_eq!(otel.ocsf_events, vec![vec![4, 5, 6]]); } else { - panic!("expected Telemetry payload for OCSF"); + panic!("expected OtelExport payload for OCSF"); } handle.await.unwrap(); diff --git a/crates/openshell-supervisor-process/src/run.rs b/crates/openshell-supervisor-process/src/run.rs index 2fa4510678..5e81d043df 100644 --- a/crates/openshell-supervisor-process/src/run.rs +++ b/crates/openshell-supervisor-process/src/run.rs @@ -90,7 +90,7 @@ pub async fn run_process( tokio::sync::mpsc::UnboundedSender, >, #[cfg(target_os = "linux")] bypass_activity_tx: Option, - telemetry_rx: Option>, + otel_rx: Option>, ) -> Result { // Platform drivers with a resolved numeric UID/GID retain the legacy // account-file update. OCI-image identity leaves those environment values @@ -379,7 +379,7 @@ pub async fn run_process( None, Arc::clone(&supervisor_terminating), main_instance_id.clone(), - telemetry_rx, + otel_rx, ); info!("supervisor session task spawned"); Some(task) diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index f2906a83a9..5547791d8a 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -275,7 +275,7 @@ fn map_session_stream_message( /// The task runs for the lifetime of the sandbox process, reconnecting with /// exponential backoff on failures. /// -/// `telemetry_rx` is an optional channel for receiving telemetry messages from +/// `otel_rx` is an optional channel for receiving OTEL export messages from /// the relay forwarder. The session drains this channel and forwards the /// messages to the gateway. pub fn spawn( @@ -286,7 +286,7 @@ pub fn spawn( expected_ssh_peer_pid: Option, terminating: Arc, instance_id: String, - telemetry_rx: Option>, + otel_rx: Option>, ) -> tokio::task::JoinHandle<()> { let config = SessionConfig { endpoint, @@ -296,7 +296,7 @@ pub fn spawn( expected_ssh_peer_pid, terminating, instance_id, - telemetry_rx, + otel_rx, }; tokio::spawn(run_session_loop(config)) } @@ -309,18 +309,18 @@ struct SessionConfig { expected_ssh_peer_pid: Option, terminating: Arc, instance_id: String, - telemetry_rx: Option>, + otel_rx: Option>, } async fn run_session_loop(mut config: SessionConfig) { let mut backoff = INITIAL_BACKOFF; let mut attempt: u64 = 0; - let mut telemetry_rx = config.telemetry_rx.take(); + let mut otel_rx = config.otel_rx.take(); loop { attempt += 1; - match run_single_session(&config, &mut telemetry_rx).await { + match run_single_session(&config, &mut otel_rx).await { Ok(()) => { let event = session_closed_event( openshell_ocsf::ctx::ctx(), @@ -347,7 +347,7 @@ async fn run_session_loop(mut config: SessionConfig) { async fn run_single_session( config: &SessionConfig, - telemetry_rx: &mut Option>, + otel_rx: &mut Option>, ) -> Result<(), Box> { // Connect to the gateway. The same `Channel` is used for both the // long-lived control stream and all data-plane `RelayStream` calls, so @@ -367,7 +367,7 @@ async fn run_single_session( payload: Some(supervisor_message::Payload::Hello(SupervisorHello { sandbox_id: config.sandbox_id.clone(), instance_id: config.instance_id.clone(), - capabilities: vec!["telemetry_relay".to_string()], + capabilities: vec!["otel_export".to_string()], })), }) .await @@ -394,12 +394,12 @@ async fn run_single_session( _ => return Err("expected SessionAccepted or SessionRejected".into()), }; - let telemetry_confirmed = accepted.capabilities.iter().any(|c| c == "telemetry_relay"); - if telemetry_confirmed { - info!("gateway confirmed telemetry_relay capability; OTLP drain active"); + let otel_confirmed = accepted.capabilities.iter().any(|c| c == "otel_export"); + if otel_confirmed { + info!("gateway confirmed otel_export capability; OTLP drain active"); } else { debug!( - "gateway did not confirm telemetry_relay; \ + "gateway did not confirm otel_export; \ OTLP forwarding disabled for this session" ); } @@ -413,7 +413,7 @@ async fn run_single_session( ); ocsf_emit!(event); - // Main loop: receive gateway messages + send heartbeats + drain telemetry. + // Main loop: receive gateway messages + send heartbeats + drain OTEL exports. let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(u64::from(heartbeat_secs))); heartbeat_interval.tick().await; // skip immediate tick @@ -453,15 +453,15 @@ async fn run_single_session( return Err("outbound channel closed".into()); } } - telemetry_msg = async { - match telemetry_rx.as_mut() { + otel_msg = async { + match otel_rx.as_mut() { Some(rx) => rx.recv().await, None => std::future::pending().await, } - }, if telemetry_confirmed => { - if let Some(msg) = telemetry_msg { + }, if otel_confirmed => { + if let Some(msg) = otel_msg { if tx.try_send(msg).is_err() { - debug!("telemetry: session channel full or closed, dropping message"); + debug!("OTEL relay: session channel full or closed, dropping message"); } } } diff --git a/proto/openshell.proto b/proto/openshell.proto index 2fbc257eab..4074c46b7a 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2358,19 +2358,23 @@ message GetSandboxLogsResponse { // Supervisor session messages // --------------------------------------------------------------------------- -// Telemetry data forwarded from supervisor to gateway. +// OpenTelemetry export data forwarded from supervisor to gateway. // Contains agent trace spans and/or OCSF events from a single sandbox. -message TelemetryData { +message OtelExportData { // Source sandbox identifier. string sandbox_id = 1; - // Serialized OTLP ExportTraceServiceRequest (protobuf encoding). - // Contains pre-enriched spans with sandbox resource attributes. - // Empty when only OCSF events are being forwarded. - bytes trace_data = 2; + // The OTLP signal being forwarded. Each variant carries the serialized + // OTLP Export*ServiceRequest in protobuf encoding, pre-enriched with + // sandbox resource attributes. + oneof signal { + bytes trace_data = 2; + // bytes metrics_data = 4; // future + // bytes logs_data = 5; // future + } // Serialized OCSF events (each entry is a JSON-encoded OCSF event). - // Empty when only trace data is being forwarded. + // Empty when only OTLP signal data is being forwarded. repeated bytes ocsf_events = 3; } @@ -2381,7 +2385,7 @@ message SupervisorMessage { SupervisorHeartbeat heartbeat = 2; RelayOpenResult relay_open_result = 3; RelayClose relay_close = 4; - TelemetryData telemetry = 5; + OtelExportData otel_export = 5; } } @@ -2402,7 +2406,7 @@ message SupervisorHello { string sandbox_id = 1; // Supervisor instance ID (e.g. boot id or process epoch). string instance_id = 2; - // Capabilities this supervisor supports (e.g. "telemetry_relay"). + // Capabilities this supervisor supports (e.g. "otel_export"). repeated string capabilities = 3; }