Skip to content

Commit c1aad7d

Browse files
author
RageLtMan
committed
Enforce Opt-In-Only Data Collection/External Calls
All external communications now require explicit opt-in via environment variables as opposed to requiring users to find and explicitly apply opt-outs from data collection. Relevant for GRC concerns such as GDPR and other regional privacy regulations as well as basic user-retention/adoption - adress sentiment concern. Summary: - Auto-update checks blocked by default (requires `STAKPAK_ENABLE_UPDATES=1`) - Machine fingerprinting blocked by default (requires `STAKPAK_GENERATE_MACHINE_ID=1`) - Telemetry payload fields blocked individually (requires `STAKPAK_ENABLE_TELEMETRY=1` + per-field opts) - Fixed interactive mode telemetry bypass (`collect_telemetry.unwrap_or(false)`) Privacy Impact: - Zero external calls unless user explicitly opts in - No persistent machine identification without consent - Granular control over telemetry data fields - Sovereign workflow enforced by default
1 parent f1f159c commit c1aad7d

File tree

7 files changed

+174
-41
lines changed

7 files changed

+174
-41
lines changed

cli/src/commands/agent/run/mode_interactive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ pub async fn run_interactive(
678678
// Capture telemetry when not using Stakpak API (local mode)
679679
if !has_stakpak_key
680680
&& let Some(ref anonymous_id) = ctx_clone.anonymous_id
681-
&& ctx_clone.collect_telemetry.unwrap_or(true)
681+
&& ctx_clone.collect_telemetry.unwrap_or(false)
682682
{
683683
capture_event(
684684
anonymous_id,

cli/src/config/file.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ impl Default for ConfigFile {
2626
settings: Settings {
2727
machine_name: None,
2828
auto_append_gitignore: Some(true),
29-
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
30-
collect_telemetry: Some(true),
29+
// DO NOT generate anonymous_id by default - only if telemetry is enabled
30+
anonymous_id: None,
31+
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
32+
collect_telemetry: Some(false),
3133
editor: Some("nano".to_string()),
3234
},
3335
}
@@ -38,15 +40,16 @@ impl ConfigFile {
3840
/// Create a config file with a default profile.
3941
pub(crate) fn with_default_profile() -> Self {
4042
ConfigFile {
41-
profiles: HashMap::from([(
42-
"default".into(),
43+
profiles: HashMap::from([("default".into(),
4344
ProfileConfig::with_api_endpoint(STAKPAK_API_ENDPOINT),
4445
)]),
4546
settings: Settings {
4647
machine_name: None,
4748
auto_append_gitignore: Some(true),
48-
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
49-
collect_telemetry: Some(true),
49+
// DO NOT generate anonymous_id by default - only if telemetry is enabled
50+
anonymous_id: None,
51+
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
52+
collect_telemetry: Some(false),
5053
editor: Some("nano".to_string()),
5154
},
5255
}
@@ -97,7 +100,9 @@ impl ConfigFile {
97100
self.settings = Settings {
98101
machine_name: config.machine_name,
99102
auto_append_gitignore: config.auto_append_gitignore,
103+
// Only set anonymous_id if config explicitly provides it (telemetry enabled)
100104
anonymous_id: config.anonymous_id.or(existing_anonymous_id),
105+
// Only set collect_telemetry if config explicitly provides it
101106
collect_telemetry: config.collect_telemetry.or(existing_collect_telemetry),
102107
editor: config.editor.or(existing_editor),
103108
};
@@ -173,11 +178,10 @@ impl From<OldAppConfig> for ConfigFile {
173178
fn from(old_config: OldAppConfig) -> Self {
174179
let settings: Settings = old_config.clone().into();
175180
ConfigFile {
176-
profiles: HashMap::from([(
177-
"default".to_string(),
181+
profiles: HashMap::from([("default".to_string(),
178182
ProfileConfig::migrated_from_old_config(old_config),
179183
)]),
180184
settings,
181185
}
182186
}
183-
}
187+
}

cli/src/config/types.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub struct Settings {
2424
#[serde(alias = "user_id")]
2525
pub anonymous_id: Option<String>,
2626
/// Whether to collect telemetry data
27+
/// **DEFAULT: false (opt-in required for privacy)**
28+
/// Users must explicitly set this to `true` to enable telemetry
2729
pub collect_telemetry: Option<bool>,
2830
/// Preferred external editor (e.g. vim, nano, code)
2931
pub editor: Option<String>,
@@ -43,9 +45,11 @@ impl From<OldAppConfig> for Settings {
4345
Settings {
4446
machine_name: old_config.machine_name,
4547
auto_append_gitignore: old_config.auto_append_gitignore,
46-
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
47-
collect_telemetry: Some(true),
48+
// Do NOT generate anonymous_id by default - only if telemetry is enabled
49+
anonymous_id: None,
50+
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
51+
collect_telemetry: Some(false),
4852
editor: Some("nano".to_string()),
4953
}
5054
}
51-
}
55+
}

cli/src/main.rs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,14 @@ async fn main() {
237237
Cli::parse()
238238
};
239239

240-
// Only run auto-update in interactive mode (when no command is specified)
241-
if cli.command.is_none()
240+
// AUTO-UPDATE: BLOCKED BY DEFAULT - requires explicit OPT-IN
241+
// Only run auto-update if user sets STAKPAK_ENABLE_UPDATES=1 (dangerous)
242+
let should_check_updates = std::env::var("STAKPAK_ENABLE_UPDATES")
243+
.unwrap_or_else(|_| "0".to_string())
244+
.eq_ignore_ascii_case("1");
245+
246+
if should_check_updates
247+
&& cli.command.is_none()
242248
&& !cli.r#async
243249
&& !cli.print
244250
&& let Err(e) = auto_update().await
@@ -291,17 +297,26 @@ async fn main() {
291297
return; // Exit after warden execution completes
292298
}
293299

294-
if config.machine_name.is_none() {
295-
// Generate a random machine name
300+
// MACHINE FINGERPRINTING: BLOCKED BY DEFAULT - requires explicit OPT-IN
301+
// Only generate machine name if user sets STAKPAK_GENERATE_MACHINE_ID=1 (dangerous)
302+
let should_generate_machine_id = std::env::var("STAKPAK_GENERATE_MACHINE_ID")
303+
.unwrap_or_else(|_| "0".to_string())
304+
.eq_ignore_ascii_case("1");
305+
306+
if should_generate_machine_id && config.machine_name.is_none() {
307+
// Generate a random machine name (user opted-in)
296308
let random_name = names::Generator::with_naming(Name::Numbered)
297309
.next()
298310
.unwrap_or_else(|| "unknown-machine".to_string());
299311

300312
config.machine_name = Some(random_name);
301313

302314
if let Err(e) = config.save() {
303-
eprintln!("Failed to save config: {}", e);
315+
eprintln!("Failed to save machine name to config: {}", e);
304316
}
317+
} else if !should_generate_machine_id && config.machine_name.is_none() {
318+
// Log warning that machine name is not generated (safe default)
319+
tracing::debug!("Machine name not generated - set STAKPAK_GENERATE_MACHINE_ID=1 to enable");
305320
}
306321

307322
// Run interactive/async agent when no subcommand or Init; otherwise run the subcommand
@@ -380,14 +395,20 @@ async fn main() {
380395
std::process::exit(1);
381396
}));
382397

383-
// Parallelize HTTP calls for faster startup
398+
// PARALLELIZE ONLY IF OPT-IN: update checks blocked by default
384399
let current_version = format!("v{}", env!("CARGO_PKG_VERSION"));
385400
let client_for_rulebooks = client.clone();
386401
let config_for_rulebooks = config.clone();
387402

388-
let (api_result, update_result, rulebooks_result) = tokio::join!(
403+
// Check updates sequentially to avoid type inference issues
404+
let update_result = if should_check_updates {
405+
check_update(&current_version).await
406+
} else {
407+
Ok(())
408+
};
409+
410+
let (api_result, rulebooks_result) = tokio::join!(
389411
client.get_my_account(),
390-
check_update(&current_version),
391412
async {
392413
client_for_rulebooks
393414
.list_rulebooks()
@@ -1025,4 +1046,4 @@ mod tests {
10251046
.requires_auth()
10261047
);
10271048
}
1028-
}
1049+
}

cli/src/onboarding/save_config.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::config::{ConfigFile, ProfileConfig, ProviderType};
44
use crate::onboarding::config_templates::config_to_toml_preview;
55
use crate::onboarding::styled_output;
6-
use stakpak_shared::telemetry::{TelemetryEvent, capture_event};
6+
use stakpak_shared::telemetry::{TelemetryEvent, capture_event, is_telemetry_enabled, is_telemetry_env_enabled};
77
use std::fs;
88
use std::path::PathBuf;
99

@@ -36,11 +36,21 @@ pub fn save_to_profile(
3636
let is_local_provider = matches!(profile.provider, Some(ProviderType::Local));
3737
let is_first_telemetry_setup = config_file.settings.anonymous_id.is_none();
3838

39-
if is_local_provider && config_file.settings.anonymous_id.is_none() {
39+
// SOVEREIGNTY GUARD: Only generate anonymous_id if telemetry is explicitly enabled
40+
// Both config AND environment variable must opt-in
41+
if is_local_provider
42+
&& config_file.settings.anonymous_id.is_none()
43+
&& is_telemetry_enabled(config_file.settings.collect_telemetry)
44+
&& is_telemetry_env_enabled()
45+
{
4046
config_file.settings.anonymous_id = Some(uuid::Uuid::new_v4().to_string());
4147
}
48+
49+
// DO NOT set collect_telemetry to true by default - maintain opt-in only
50+
// If user hasn't explicitly set it, keep it as None (will default to false)
4251
if is_local_provider && config_file.settings.collect_telemetry.is_none() {
43-
config_file.settings.collect_telemetry = Some(true);
52+
// Never auto-enable telemetry - keep it disabled by default
53+
config_file.settings.collect_telemetry = Some(false);
4454
}
4555

4656
config_file
@@ -56,15 +66,19 @@ pub fn save_to_profile(
5666
.save_to(&path)
5767
.map_err(|e| format!("Failed to save config file: {}", e))?;
5868

69+
// SOVEREIGNTY GUARD: Only capture telemetry if user explicitly enabled it
70+
// Requires: config opt-in AND env var opt-in AND anonymous_id generated
5971
if is_local_provider
6072
&& is_first_telemetry_setup
73+
&& is_telemetry_enabled(config_file.settings.collect_telemetry)
74+
&& is_telemetry_env_enabled()
75+
&& config_file.settings.collect_telemetry.unwrap_or(false)
6176
&& let Some(ref anonymous_id) = config_file.settings.anonymous_id
62-
&& config_file.settings.collect_telemetry.unwrap_or(true)
6377
{
6478
capture_event(
6579
anonymous_id,
6680
config_file.settings.machine_name.as_deref(),
67-
true,
81+
true, // telemetry is enabled
6882
TelemetryEvent::FirstOpen,
6983
);
7084
}
@@ -95,4 +109,4 @@ pub fn preview_and_save_to_profile(
95109
println!();
96110

97111
Ok(telemetry_settings)
98-
}
112+
}

cli/src/utils/check_update.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ use serde::Deserialize;
44
use stakpak_shared::tls_client::{TlsClientConfig, create_tls_client};
55
use std::error::Error;
66

7+
// UPDATE CHECKS: DEFAULT DISABLED - requires STAKPAK_ENABLE_UPDATES=1
8+
// This prevents mandatory external calls during startup
9+
const UPDATE_CHECK_ENABLED: &str = "STAKPAK_ENABLE_UPDATES";
10+
711
use crate::commands::auto_update::run_auto_update;
812
use crate::utils::cli_colors::CliColors;
913

14+
/// Check if update checks are enabled (default: disabled for sovereignty)
15+
fn is_update_check_enabled() -> bool {
16+
std::env::var(UPDATE_CHECK_ENABLED)
17+
.unwrap_or_else(|_| "0".to_string())
18+
.eq_ignore_ascii_case("1")
19+
}
20+
1021
/// Parse version string (with or without 'v' prefix) into semver Version
1122
fn parse_version(version_str: &str) -> Option<Version> {
1223
let cleaned = version_str.strip_prefix('v').unwrap_or(version_str);
@@ -125,6 +136,11 @@ fn format_changelog(body: &str) -> String {
125136
}
126137

127138
pub async fn check_update(current_version: &str) -> Result<(), Box<dyn Error>> {
139+
// BLOCKED BY DEFAULT - requires explicit opt-in
140+
if !is_update_check_enabled() {
141+
return Ok(());
142+
}
143+
128144
let release = get_latest_release().await?;
129145
if is_newer_version(current_version, &release.tag_name) {
130146
let blue = CliColors::blue();
@@ -141,33 +157,32 @@ pub async fn check_update(current_version: &str) -> Result<(), Box<dyn Error>> {
141157
"{}┃{}{}⮕ {} Version Update Available!{}{}┃{}",
142158
blue, reset, cyan, text, reset, blue, reset
143159
);
144-
println!("{}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛{}", blue, reset);
145160
println!(
146161
"{} {}{}{} → {}{}{}",
147162
text, yellow, current_version, reset, green, release.tag_name, reset
148163
);
149-
println!("{}", sep);
164+
println!("{}{}", sep, reset);
150165

151166
if let Some(body) = &release.body
152167
&& !body.trim().is_empty()
153168
{
154169
println!("{} What's new in this update:{}", text, reset);
155-
println!("{}", sep);
170+
println!("{}{}", sep, reset);
156171
let changelog = format_changelog(body);
157-
println!("{}", changelog);
158-
println!("{}", sep);
172+
println!("{}{}", changelog, reset);
173+
println!("{}{}", sep, reset);
159174
println!(
160175
"{} View full changelog: {}{}{}{}",
161176
text, reset, cyan, release.html_url, reset
162177
);
163-
println!("{}", sep);
178+
println!("{}{}", sep, reset);
164179
}
165180

166181
println!(
167182
"{} Upgrade to access the latest features! 🚀{}",
168183
text, reset
169184
);
170-
println!("{}", sep);
185+
println!("{}{}", sep, reset);
171186
}
172187

173188
Ok(())
@@ -197,6 +212,11 @@ pub async fn get_latest_cli_version() -> Result<String, Box<dyn Error>> {
197212
}
198213

199214
pub async fn auto_update() -> Result<(), Box<dyn Error>> {
215+
// BLOCKED BY DEFAULT - requires explicit opt-in
216+
if !is_update_check_enabled() {
217+
return Ok(());
218+
}
219+
200220
let release = get_latest_release().await?;
201221
let current_version = format!("v{}", env!("CARGO_PKG_VERSION"));
202222
if is_newer_version(&current_version, &release.tag_name) {
@@ -217,7 +237,7 @@ pub async fn auto_update() -> Result<(), Box<dyn Error>> {
217237
println!("{} What's new in this update:{}", text, reset);
218238
println!("{}{}{}", cyan, "─".repeat(50), reset);
219239
let changelog = format_changelog(body);
220-
println!("{}", changelog);
240+
println!("{}{}", changelog, reset);
221241
println!("{}{}{}", cyan, "─".repeat(50), reset);
222242
println!(
223243
"{} View full changelog: {}{}{}\n",
@@ -246,6 +266,11 @@ pub async fn auto_update() -> Result<(), Box<dyn Error>> {
246266
/// Force auto-update without prompting (for ACP mode).
247267
/// Returns true if an update was performed and the process should restart.
248268
pub async fn force_auto_update() -> Result<bool, Box<dyn Error>> {
269+
// BLOCKED BY DEFAULT - requires explicit opt-in
270+
if !is_update_check_enabled() {
271+
return Ok(false);
272+
}
273+
249274
let release = get_latest_release().await?;
250275
let current_version = format!("v{}", env!("CARGO_PKG_VERSION"));
251276
if is_newer_version(&current_version, &release.tag_name) {
@@ -260,4 +285,4 @@ pub async fn force_auto_update() -> Result<bool, Box<dyn Error>> {
260285
} else {
261286
Ok(false)
262287
}
263-
}
288+
}

0 commit comments

Comments
 (0)