Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/branch-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
19 changes: 19 additions & 0 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-bootstrap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
9 changes: 7 additions & 2 deletions crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
93 changes: 90 additions & 3 deletions crates/openshell-core/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<mpsc::SyncSender<TelemetryEvent>>> = OnceLock::new();

#[cfg(feature = "telemetry")]
#[derive(Debug)]
struct TelemetryEvent {
endpoint: String,
Expand Down Expand Up @@ -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"
Expand All @@ -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!(
Expand All @@ -301,6 +333,7 @@ fn telemetry_enabled_from(value: Option<&str>) -> bool {
)
}

#[cfg(feature = "telemetry")]
fn telemetry_endpoint() -> Option<String> {
telemetry_endpoint_from(
std::env::var("OPENSHELL_TELEMETRY_ENDPOINT")
Expand All @@ -309,6 +342,7 @@ fn telemetry_endpoint() -> Option<String> {
)
}

#[cfg(feature = "telemetry")]
fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option<String> {
let endpoint = endpoint.unwrap_or(DEFAULT_ENDPOINT);
let endpoint = endpoint.trim();
Expand All @@ -319,14 +353,17 @@ fn telemetry_endpoint_from(endpoint: Option<&str>) -> Option<String> {
}
}

#[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",
Expand Down Expand Up @@ -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<TelemetryEvent>> {
TELEMETRY_SENDER
.get_or_init(|| {
Expand All @@ -381,13 +419,15 @@ fn telemetry_sender() -> Option<&'static mpsc::SyncSender<TelemetryEvent>> {
.as_ref()
}

#[cfg(feature = "telemetry")]
fn telemetry_worker(rx: mpsc::Receiver<TelemetryEvent>) {
for event in rx {
let payload = build_payload(event.name, event.event, &event.event_ts, &timestamp());
let _ = publish_payload(&event.endpoint, payload);
}
}

#[cfg(feature = "telemetry")]
fn publish_payload(endpoint: &str, payload: Value) -> Result<(), reqwest::Error> {
Client::builder()
.use_rustls_tls()
Expand All @@ -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<TelemetryEvent>, event: TelemetryEvent) -> bool {
sender.try_send(event).is_ok()
}

#[cfg(feature = "telemetry")]
fn emit_event(name: &'static str, event: Value) {
if !enabled() {
return;
Expand All @@ -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,
Expand Down Expand Up @@ -563,7 +609,7 @@ where
Some(sanitized)
}

#[cfg(test)]
#[cfg(all(test, feature = "telemetry"))]
mod tests {
use super::*;

Expand Down Expand Up @@ -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)]);
}
}
2 changes: 1 addition & 1 deletion crates/openshell-driver-docker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-kubernetes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-podman/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
9 changes: 8 additions & 1 deletion crates/openshell-driver-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-policy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading
Loading