From f33b3813138adbf6c660c7bcfbe00fb3c0236efd Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Tue, 9 Jun 2026 15:39:16 -0400 Subject: [PATCH 1/2] feat(telemetry): add build-time option to compile out telemetry Gate anonymous telemetry emission behind a default-on `telemetry` Cargo feature in openshell-core. The data model (enums, validation, emit_*/enabled* signatures) stays always-compiled, while the endpoint, HTTP client, queue, and emission code are feature-gated. With the feature off, enabled() returns false and emit_* are no-ops, so dependent crates compile unchanged and no telemetry endpoint, HTTP client, or emission code is included in the binary. chrono and reqwest become optional dependencies of openshell-core, dropped from its dependency graph when telemetry is disabled. Thread the switch through the workspace: every crate depends on openshell-core with default-features = false, and the default-on `telemetry` passthrough lives on the binary crates that emit or collect telemetry (openshell-server, openshell-sandbox, openshell-driver-vm). In-process drivers inherit it via resolver v2 feature unification. Build a telemetry-free binary with, e.g.: cargo build --release -p openshell-server --no-default-features The runtime OPENSHELL_TELEMETRY_ENABLED switch is unchanged for default builds. Signed-off-by: Russell Bryant --- README.md | 4 +- architecture/build.md | 19 ++++ crates/openshell-bootstrap/Cargo.toml | 2 +- crates/openshell-cli/Cargo.toml | 2 +- crates/openshell-core/Cargo.toml | 9 +- crates/openshell-core/src/telemetry.rs | 93 ++++++++++++++++++- crates/openshell-driver-docker/Cargo.toml | 2 +- crates/openshell-driver-kubernetes/Cargo.toml | 2 +- crates/openshell-driver-podman/Cargo.toml | 2 +- crates/openshell-driver-vm/Cargo.toml | 9 +- crates/openshell-policy/Cargo.toml | 2 +- crates/openshell-providers/Cargo.toml | 2 +- crates/openshell-router/Cargo.toml | 2 +- crates/openshell-sandbox/Cargo.toml | 9 +- crates/openshell-sandbox/src/lib.rs | 1 + crates/openshell-server/Cargo.toml | 7 +- crates/openshell-tui/Cargo.toml | 2 +- 17 files changed, 151 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4fe242d93..d36032c3c 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,9 @@ OpenShell is built agent-first — your agent is your first collaborator. Before OpenShell collects anonymous telemetry to help improve the project for developers. This data is not used to track individual user behavior. It helps us understand aggregate usage of sandbox, provider, and policy workflows so we can prioritize product improvements and share usage trends with the community. -Disable telemetry by setting `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway deployment. OpenShell propagates this deployment setting into sandbox supervisor environments so sandbox-side telemetry collection is disabled as well. +Disable telemetry at runtime by setting `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway deployment. OpenShell propagates this deployment setting into sandbox supervisor environments so sandbox-side telemetry collection is disabled as well. + +You can also compile telemetry out entirely. Telemetry support is a default-on `telemetry` Cargo feature; building with `--no-default-features` produces binaries that contain no telemetry endpoint, no telemetry HTTP client, and no emission code. Build telemetry-free artifacts with, for example, `cargo build --release -p openshell-server --no-default-features` (gateway) and the equivalent for `openshell-sandbox` and `openshell-driver-vm`. With telemetry compiled out, the gateway emits nothing and reports telemetry disabled to the sandboxes it launches. Telemetry events are limited to anonymous operational categories and counts, such as sandbox lifecycle outcomes, provider profile buckets, policy decision counts, and aggregate network activity denial categories. OpenShell telemetry does not collect sandbox names or IDs, hostnames, file paths, binary paths, prompts, credentials, provider names, model names, or user content. diff --git a/architecture/build.md b/architecture/build.md index 200be8b1e..012a72d3f 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -20,6 +20,25 @@ OpenShell builds these main artifacts: Sandbox community images are built outside this repository. +## Build Features + +Anonymous telemetry emission is gated behind a default-on `telemetry` Cargo +feature. It is defined in `openshell-core` (where the emission code, HTTP +client, and endpoint live) and forwarded by the binary crates that emit or +collect telemetry: `openshell-server` (gateway), `openshell-sandbox` +(supervisor), and `openshell-driver-vm`. Every crate depends on +`openshell-core` with `default-features = false`, so the binary crate's feature +is the single switch that enables `openshell-core/telemetry` for its build +graph. In-process drivers (`docker`, `kubernetes`, `podman`) inherit the +gateway's setting through feature unification and carry no passthrough. + +Building a binary with `--no-default-features` compiles out telemetry entirely: +no endpoint, no telemetry HTTP client, and no emission code. With telemetry +compiled out, `telemetry::enabled()` is always `false` and the `emit_*` helpers +are no-ops, so the data-model types stay available and dependent crates compile +unchanged. The runtime `OPENSHELL_TELEMETRY_ENABLED` switch remains the way to +disable telemetry in a default (telemetry-enabled) build. + ## Linux Runtime Environments OpenShell uses different Linux libc environments for different host artifacts. diff --git a/crates/openshell-bootstrap/Cargo.toml b/crates/openshell-bootstrap/Cargo.toml index 578d59e65..c860cb138 100644 --- a/crates/openshell-bootstrap/Cargo.toml +++ b/crates/openshell-bootstrap/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } bollard = "0.20" bytes = { workspace = true } futures = { workspace = true } diff --git a/crates/openshell-cli/Cargo.toml b/crates/openshell-cli/Cargo.toml index b69a9629b..4d7241de3 100644 --- a/crates/openshell-cli/Cargo.toml +++ b/crates/openshell-cli/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] openshell-bootstrap = { path = "../openshell-bootstrap" } -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } openshell-policy = { path = "../openshell-policy" } openshell-providers = { path = "../openshell-providers" } openshell-prover = { path = "../openshell-prover" } diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index 78c87d54c..469a0f4d9 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -20,10 +20,15 @@ serde = { workspace = true } serde_json = { workspace = true } url = { workspace = true } ipnet = "2" -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } -reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"] } +chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } +reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"], optional = true } [features] +default = ["telemetry"] +## Compile in anonymous telemetry emission support. On by default; disable with +## `--no-default-features` (plus any other features you need) for a build that +## contains no telemetry endpoint, no HTTP client, and no emission code at all. +telemetry = ["dep:reqwest", "dep:chrono"] ## Include test-only settings (dummy_bool, dummy_int) in the registry. ## Off by default so production builds have an empty registry. ## Enabled by e2e tests and during development. diff --git a/crates/openshell-core/src/telemetry.rs b/crates/openshell-core/src/telemetry.rs index e4dc0c37e..96f68d35c 100644 --- a/crates/openshell-core/src/telemetry.rs +++ b/crates/openshell-core/src/telemetry.rs @@ -3,25 +3,40 @@ //! Best-effort anonymous telemetry emission helpers. +#[cfg(feature = "telemetry")] use chrono::{SecondsFormat, Utc}; +#[cfg(feature = "telemetry")] use reqwest::blocking::Client; use serde_json::{Value, json}; use std::collections::BTreeMap; +#[cfg(feature = "telemetry")] use std::sync::{OnceLock, mpsc}; +#[cfg(feature = "telemetry")] use std::thread; +#[cfg(feature = "telemetry")] use std::time::Duration; -const TELEMETRY_EVENT_QUEUE_CAPACITY: usize = 1024; const MAX_TELEMETRY_INTEGER: u64 = 9_223_372_036_854_775_807; +const SOURCE: TelemetrySource = TelemetrySource::OpenShell; + +#[cfg(feature = "telemetry")] +const TELEMETRY_EVENT_QUEUE_CAPACITY: usize = 1024; +#[cfg(feature = "telemetry")] const CLIENT_ID: &str = "415437562476676"; +#[cfg(feature = "telemetry")] const DEFAULT_ENDPOINT: &str = "https://events.telemetry.data.nvidia.com/v1.1/events/json"; +#[cfg(feature = "telemetry")] const EVENT_SCHEMA_VERSION: &str = "4.0"; +#[cfg(feature = "telemetry")] const EVENT_PROTOCOL_VERSION: &str = "1.6"; +#[cfg(feature = "telemetry")] const EVENT_SYSTEM_VERSION: &str = "openshell-telemetry/1.0"; +#[cfg(feature = "telemetry")] const HTTP_TIMEOUT: Duration = Duration::from_secs(5); -const SOURCE: TelemetrySource = TelemetrySource::OpenShell; +#[cfg(feature = "telemetry")] static TELEMETRY_SENDER: OnceLock>> = OnceLock::new(); +#[cfg(feature = "telemetry")] #[derive(Debug)] struct TelemetryEvent { endpoint: String, @@ -277,14 +292,30 @@ impl DenyGroup { } } +#[cfg(feature = "telemetry")] pub fn enabled() -> bool { telemetry_enabled_from(std::env::var("OPENSHELL_TELEMETRY_ENABLED").ok().as_deref()) } +/// Telemetry support is compiled out: always disabled. +#[cfg(not(feature = "telemetry"))] +pub fn enabled() -> bool { + false +} + +#[cfg(feature = "telemetry")] pub fn enabled_env_value() -> &'static str { enabled_env_value_from(std::env::var("OPENSHELL_TELEMETRY_ENABLED").ok().as_deref()) } +/// Telemetry support is compiled out: report disabled so sandbox supervisors +/// inherit the disabled state and skip activity collection. +#[cfg(not(feature = "telemetry"))] +pub fn enabled_env_value() -> &'static str { + "false" +} + +#[cfg(feature = "telemetry")] fn enabled_env_value_from(value: Option<&str>) -> &'static str { if telemetry_enabled_from(value) { "true" @@ -293,6 +324,7 @@ fn enabled_env_value_from(value: Option<&str>) -> &'static str { } } +#[cfg(feature = "telemetry")] fn telemetry_enabled_from(value: Option<&str>) -> bool { let value = value.unwrap_or("true"); !matches!( @@ -301,6 +333,7 @@ fn telemetry_enabled_from(value: Option<&str>) -> bool { ) } +#[cfg(feature = "telemetry")] fn telemetry_endpoint() -> Option { telemetry_endpoint_from( std::env::var("OPENSHELL_TELEMETRY_ENDPOINT") @@ -309,6 +342,7 @@ fn telemetry_endpoint() -> Option { ) } +#[cfg(feature = "telemetry")] fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option { let endpoint = endpoint.unwrap_or(DEFAULT_ENDPOINT); let endpoint = endpoint.trim(); @@ -319,14 +353,17 @@ fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option { } } +#[cfg(feature = "telemetry")] fn timestamp() -> String { Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) } +#[cfg(feature = "telemetry")] fn client_version() -> &'static str { crate::VERSION } +#[cfg(feature = "telemetry")] fn build_payload(name: &str, event: Value, event_ts: &str, sent_ts: &str) -> Value { json!({ "browserType": "undefined", @@ -368,6 +405,7 @@ fn build_payload(name: &str, event: Value, event_ts: &str, sent_ts: &str) -> Val }) } +#[cfg(feature = "telemetry")] fn telemetry_sender() -> Option<&'static mpsc::SyncSender> { TELEMETRY_SENDER .get_or_init(|| { @@ -381,6 +419,7 @@ fn telemetry_sender() -> Option<&'static mpsc::SyncSender> { .as_ref() } +#[cfg(feature = "telemetry")] fn telemetry_worker(rx: mpsc::Receiver) { for event in rx { let payload = build_payload(event.name, event.event, &event.event_ts, ×tamp()); @@ -388,6 +427,7 @@ fn telemetry_worker(rx: mpsc::Receiver) { } } +#[cfg(feature = "telemetry")] fn publish_payload(endpoint: &str, payload: Value) -> Result<(), reqwest::Error> { Client::builder() .use_rustls_tls() @@ -401,10 +441,12 @@ fn publish_payload(endpoint: &str, payload: Value) -> Result<(), reqwest::Error> Ok(()) } +#[cfg(feature = "telemetry")] fn try_enqueue_event(sender: &mpsc::SyncSender, event: TelemetryEvent) -> bool { sender.try_send(event).is_ok() } +#[cfg(feature = "telemetry")] fn emit_event(name: &'static str, event: Value) { if !enabled() { return; @@ -427,6 +469,10 @@ fn emit_event(name: &'static str, event: Value) { ); } +/// Telemetry support is compiled out: emission is a no-op. +#[cfg(not(feature = "telemetry"))] +fn emit_event(_name: &'static str, _event: Value) {} + pub fn emit_lifecycle( resource: LifecycleResource, operation: LifecycleOperation, @@ -563,7 +609,7 @@ where Some(sanitized) } -#[cfg(test)] +#[cfg(all(test, feature = "telemetry"))] mod tests { use super::*; @@ -709,3 +755,44 @@ mod tests { assert_eq!(rows.get(&DenyGroup::Unknown), Some(&3)); } } + +#[cfg(all(test, not(feature = "telemetry")))] +mod disabled_tests { + use super::*; + + #[test] + fn telemetry_reports_disabled_when_compiled_out() { + assert!(!enabled()); + assert_eq!(enabled_env_value(), "false"); + } + + #[test] + fn emit_functions_are_no_ops_when_compiled_out() { + // These must remain callable so dependent crates compile unchanged; + // with telemetry compiled out they do nothing and never panic. + emit_lifecycle( + LifecycleResource::Sandbox, + LifecycleOperation::Create, + TelemetryOutcome::Success, + ); + emit_provider_lifecycle( + LifecycleOperation::Create, + TelemetryOutcome::Success, + ProviderProfile::Custom, + ); + emit_sandbox_create( + TelemetryOutcome::Success, + false, + 1, + false, + SandboxTemplateSource::Default, + TelemetryComputeDriver::Docker, + ); + emit_policy_decision( + PolicyDecisionOperation::Approve, + TelemetryOutcome::Success, + 1, + ); + emit_sandbox_activity_summary(0, 0, 0.0, [(DenyGroup::ConnectPolicy, 0)]); + } +} diff --git a/crates/openshell-driver-docker/Cargo.toml b/crates/openshell-driver-docker/Cargo.toml index fb2a643ea..4ddb1a913 100644 --- a/crates/openshell-driver-docker/Cargo.toml +++ b/crates/openshell-driver-docker/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } tokio = { workspace = true } tonic = { workspace = true } diff --git a/crates/openshell-driver-kubernetes/Cargo.toml b/crates/openshell-driver-kubernetes/Cargo.toml index 885c64944..07fa91015 100644 --- a/crates/openshell-driver-kubernetes/Cargo.toml +++ b/crates/openshell-driver-kubernetes/Cargo.toml @@ -15,7 +15,7 @@ name = "openshell-driver-kubernetes" path = "src/main.rs" [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } tokio = { workspace = true } tonic = { workspace = true, features = ["transport"] } diff --git a/crates/openshell-driver-podman/Cargo.toml b/crates/openshell-driver-podman/Cargo.toml index 6f2963d92..4a1c8de83 100644 --- a/crates/openshell-driver-podman/Cargo.toml +++ b/crates/openshell-driver-podman/Cargo.toml @@ -15,7 +15,7 @@ name = "openshell-driver-podman" path = "src/main.rs" [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } tokio = { workspace = true } tonic = { workspace = true, features = ["transport"] } diff --git a/crates/openshell-driver-vm/Cargo.toml b/crates/openshell-driver-vm/Cargo.toml index a601e982b..8436be194 100644 --- a/crates/openshell-driver-vm/Cargo.toml +++ b/crates/openshell-driver-vm/Cargo.toml @@ -19,7 +19,7 @@ name = "openshell-driver-vm" path = "src/main.rs" [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } openshell-vfio = { path = "../openshell-vfio" } bollard = { version = "0.20", features = ["ssh"] } @@ -46,6 +46,13 @@ flate2 = "1" sha2 = "0.10" zstd = "0.13" +[features] +default = ["telemetry"] +## Compile in telemetry support (forwards to openshell-core/telemetry). On by +## default; build with `--no-default-features` for a telemetry-free VM driver +## that reports telemetry disabled to the sandboxes it launches. +telemetry = ["openshell-core/telemetry"] + [dev-dependencies] temp-env = "0.3" diff --git a/crates/openshell-policy/Cargo.toml b/crates/openshell-policy/Cargo.toml index 8936b85be..16719de13 100644 --- a/crates/openshell-policy/Cargo.toml +++ b/crates/openshell-policy/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } serde = { workspace = true } serde_json = { workspace = true } serde_yml = { workspace = true } diff --git a/crates/openshell-providers/Cargo.toml b/crates/openshell-providers/Cargo.toml index e82574d73..2c9c48b63 100644 --- a/crates/openshell-providers/Cargo.toml +++ b/crates/openshell-providers/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } serde = { workspace = true } serde_json = { workspace = true } serde_yml = { workspace = true } diff --git a/crates/openshell-router/Cargo.toml b/crates/openshell-router/Cargo.toml index e4c3d5ea7..97bbf4dc7 100644 --- a/crates/openshell-router/Cargo.toml +++ b/crates/openshell-router/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } bytes = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/openshell-sandbox/Cargo.toml b/crates/openshell-sandbox/Cargo.toml index 6d527bc53..cf98193a2 100644 --- a/crates/openshell-sandbox/Cargo.toml +++ b/crates/openshell-sandbox/Cargo.toml @@ -15,7 +15,7 @@ name = "openshell-sandbox" path = "src/main.rs" [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } openshell-ocsf = { path = "../openshell-ocsf" } openshell-policy = { path = "../openshell-policy" } openshell-router = { path = "../openshell-router" } @@ -89,6 +89,13 @@ seccompiler = "0.5" tempfile = "3" uuid = { version = "1", features = ["v4"] } +[features] +default = ["telemetry"] +## Compile in telemetry activity collection (forwards to openshell-core/telemetry). +## On by default; build with `--no-default-features` for a telemetry-free sandbox +## supervisor that never collects or forwards activity summaries. +telemetry = ["openshell-core/telemetry"] + [dev-dependencies] tempfile = "3" temp-env = "0.3" diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 231b588ea..fa4654243 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -3353,6 +3353,7 @@ filesystem_policy: }); } + #[cfg(feature = "telemetry")] #[test] fn telemetry_enabled_creates_activity_collection_and_flush_channel() { let _guard = ENV_LOCK.lock().unwrap(); diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 0b7e3a97e..3fc746c6d 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] openshell-bootstrap = { path = "../openshell-bootstrap" } -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } openshell-driver-docker = { path = "../openshell-driver-docker" } openshell-driver-kubernetes = { path = "../openshell-driver-kubernetes" } openshell-driver-podman = { path = "../openshell-driver-podman" } @@ -96,6 +96,11 @@ rustix = { workspace = true } x509-parser = "0.16" [features] +default = ["telemetry"] +## Compile in anonymous telemetry emission (forwards to openshell-core/telemetry). +## On by default; build with `--no-default-features` for a telemetry-free gateway +## that contains no telemetry endpoint, HTTP client, or emission code. +telemetry = ["openshell-core/telemetry"] bundled-z3 = ["openshell-prover/bundled-z3"] dev-settings = ["openshell-core/dev-settings"] test-support = [] diff --git a/crates/openshell-tui/Cargo.toml b/crates/openshell-tui/Cargo.toml index b0ac0c7ca..71e3935f4 100644 --- a/crates/openshell-tui/Cargo.toml +++ b/crates/openshell-tui/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", default-features = false } openshell-bootstrap = { path = "../openshell-bootstrap" } openshell-policy = { path = "../openshell-policy" } openshell-providers = { path = "../openshell-providers" } From 3869cec413f756e6f35216c9e004b19edbaab93e Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Tue, 9 Jun 2026 15:46:38 -0400 Subject: [PATCH 2/2] ci(telemetry): guard that telemetry can be compiled out Add tasks/scripts/verify-telemetry-compiled-out.sh, which inspects a built binary for telemetry markers (the telemetry endpoint host and client ID) that exist only when emission code is compiled in. The rust:verify:telemetry-off mise task builds the gateway with default features (positive control: markers must be present, so the absent checks can never be silently vacuous) and with --no-default-features (markers must be absent), and checks the --no-default-features sandbox binary as well. Wire the task into the Rust branch-checks job so a regression that reintroduces telemetry code into a --no-default-features build fails CI. Signed-off-by: Russell Bryant --- .github/workflows/branch-checks.yml | 3 + tasks/rust.toml | 14 ++++ .../scripts/verify-telemetry-compiled-out.sh | 73 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100755 tasks/scripts/verify-telemetry-compiled-out.sh diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index d760d590a..de7821874 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -122,6 +122,9 @@ jobs: - name: Test run: mise run test:rust + - name: Verify telemetry can be compiled out + run: mise run rust:verify:telemetry-off + - name: sccache stats if: always() run: | diff --git a/tasks/rust.toml b/tasks/rust.toml index 2972957fc..a8856377f 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -25,3 +25,17 @@ hide = true description = "Check Rust formatting" run = "cargo fmt --all -- --check" hide = true + +["rust:verify:telemetry-off"] +description = "Verify telemetry emission code is compiled out with --no-default-features" +run = [ + # Positive control: the default (telemetry-on) gateway must contain the + # markers, so the absent checks below can never become silently vacuous. + "cargo build -p openshell-server --bin openshell-gateway", + "tasks/scripts/verify-telemetry-compiled-out.sh present target/debug/openshell-gateway", + # Guard: telemetry-free builds must contain no telemetry markers. + "cargo build -p openshell-server --bin openshell-gateway --no-default-features", + "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-gateway", + "cargo build -p openshell-sandbox --bin openshell-sandbox --no-default-features", + "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-sandbox", +] diff --git a/tasks/scripts/verify-telemetry-compiled-out.sh b/tasks/scripts/verify-telemetry-compiled-out.sh new file mode 100755 index 000000000..22a238ea1 --- /dev/null +++ b/tasks/scripts/verify-telemetry-compiled-out.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Verify whether telemetry emission code is present in a compiled binary. +# +# The `telemetry` Cargo feature (default-on, defined in openshell-core) gates the +# telemetry endpoint, HTTP client, and emission code. Building with +# --no-default-features must produce a binary that contains none of it. This +# guard inspects a built binary for telemetry markers that only exist when the +# emission code is compiled in. + +set -euo pipefail + +# Markers that appear only in compiled-in telemetry emission code. Sourced from +# crates/openshell-core/src/telemetry.rs (DEFAULT_ENDPOINT host and CLIENT_ID). +# Keep in sync with that file; the `present` positive control fails loudly if a +# marker goes stale, so the `absent` checks can never become silently vacuous. +MARKERS=( + "events.telemetry.data.nvidia.com" + "415437562476676" +) + +usage() { + echo "Usage: verify-telemetry-compiled-out.sh [binary ...]" >&2 + echo " present assert telemetry markers ARE present (positive control for a telemetry-enabled build)" >&2 + echo " absent assert telemetry markers are NOT present (telemetry compiled out)" >&2 +} + +if [[ $# -lt 2 ]]; then + usage + exit 2 +fi + +mode=$1 +shift +case "$mode" in + present | absent) ;; + *) + usage + exit 2 + ;; +esac + +if ! command -v strings >/dev/null 2>&1; then + echo "error: 'strings' (binutils) is required to inspect the binary" >&2 + exit 2 +fi + +failed=0 +for binary in "$@"; do + if [[ ! -f $binary ]]; then + echo "error: binary not found: $binary" >&2 + failed=1 + continue + fi + + dump=$(strings -a "$binary") + for marker in "${MARKERS[@]}"; do + count=$(grep -c -F "$marker" <<<"$dump" || true) + if [[ $mode == absent && $count -ne 0 ]]; then + echo "FAIL: telemetry marker '$marker' found in $binary ($count occurrence(s)); telemetry was not compiled out" >&2 + failed=1 + elif [[ $mode == present && $count -eq 0 ]]; then + echo "FAIL: telemetry marker '$marker' missing from $binary; positive control failed (marker stale or build misconfigured)" >&2 + failed=1 + else + echo "OK: marker '$marker' $mode in $(basename "$binary") ($count occurrence(s))" + fi + done +done + +exit "$failed"