Skip to content
Open
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
7 changes: 7 additions & 0 deletions GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ stakpak --disable-secret-redaction
### Privacy Mode
- Redacts IP addresses, AWS account IDs, and other sensitive data
- Perfect for sharing logs or screenshots
### Privacy-First Defaults

- Auto-update checks, machine fingerprinting, and telemetry collection are disabled by default
- Requires explicit opt-in via environment variables:
- `STAKPAK_ENABLE_UPDATES=1` for update checks
- `STAKPAK_GENERATE_MACHINE_ID=1` for machine fingerprinting
- `STAKPAK_ENABLE_TELEMETRY=1` for telemetry collection

## 🛠️ Core Capabilities

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Full setup guide: [cli/README.md](cli/README.md)
- **Dynamic Secret Substitution** - AI can read/write/compare secrets without seeing actual values
- **Secure Password Generation** - Generate cryptographically secure passwords with configurable complexity
- **Privacy Mode** - Redacts sensitive data like IP addresses and AWS account IDs
- **Privacy-First Defaults** - Auto-update checks, machine fingerprinting, and telemetry collection are disabled by default and require explicit opt-in via environment variables (`STAKPAK_ENABLE_UPDATES=1`, `STAKPAK_GENERATE_MACHINE_ID=1`, `STAKPAK_ENABLE_TELEMETRY=1`)

## 🛠️ Built for DevOps Work

Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/agent/run/mode_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ pub async fn run_interactive(
// Capture telemetry when not using Stakpak API (local mode)
if !has_stakpak_key
&& let Some(ref anonymous_id) = ctx_clone.anonymous_id
&& ctx_clone.collect_telemetry.unwrap_or(true)
&& ctx_clone.collect_telemetry.unwrap_or(false)
{
capture_event(
anonymous_id,
Expand Down
22 changes: 13 additions & 9 deletions cli/src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ impl Default for ConfigFile {
settings: Settings {
machine_name: None,
auto_append_gitignore: Some(true),
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
collect_telemetry: Some(true),
// DO NOT generate anonymous_id by default - only if telemetry is enabled
anonymous_id: None,
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
collect_telemetry: Some(false),
editor: Some("nano".to_string()),
},
}
Expand All @@ -38,15 +40,16 @@ impl ConfigFile {
/// Create a config file with a default profile.
pub(crate) fn with_default_profile() -> Self {
ConfigFile {
profiles: HashMap::from([(
"default".into(),
profiles: HashMap::from([("default".into(),
ProfileConfig::with_api_endpoint(STAKPAK_API_ENDPOINT),
)]),
settings: Settings {
machine_name: None,
auto_append_gitignore: Some(true),
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
collect_telemetry: Some(true),
// DO NOT generate anonymous_id by default - only if telemetry is enabled
anonymous_id: None,
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
collect_telemetry: Some(false),
editor: Some("nano".to_string()),
},
}
Expand Down Expand Up @@ -97,7 +100,9 @@ impl ConfigFile {
self.settings = Settings {
machine_name: config.machine_name,
auto_append_gitignore: config.auto_append_gitignore,
// Only set anonymous_id if config explicitly provides it (telemetry enabled)
anonymous_id: config.anonymous_id.or(existing_anonymous_id),
// Only set collect_telemetry if config explicitly provides it
collect_telemetry: config.collect_telemetry.or(existing_collect_telemetry),
editor: config.editor.or(existing_editor),
};
Expand Down Expand Up @@ -173,11 +178,10 @@ impl From<OldAppConfig> for ConfigFile {
fn from(old_config: OldAppConfig) -> Self {
let settings: Settings = old_config.clone().into();
ConfigFile {
profiles: HashMap::from([(
"default".to_string(),
profiles: HashMap::from([("default".to_string(),
ProfileConfig::migrated_from_old_config(old_config),
)]),
settings,
}
}
}
}
10 changes: 7 additions & 3 deletions cli/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub struct Settings {
#[serde(alias = "user_id")]
pub anonymous_id: Option<String>,
/// Whether to collect telemetry data
/// **DEFAULT: false (opt-in required for privacy)**
/// Users must explicitly set this to `true` to enable telemetry
pub collect_telemetry: Option<bool>,
/// Preferred external editor (e.g. vim, nano, code)
pub editor: Option<String>,
Expand All @@ -43,9 +45,11 @@ impl From<OldAppConfig> for Settings {
Settings {
machine_name: old_config.machine_name,
auto_append_gitignore: old_config.auto_append_gitignore,
anonymous_id: Some(uuid::Uuid::new_v4().to_string()),
collect_telemetry: Some(true),
// Do NOT generate anonymous_id by default - only if telemetry is enabled
anonymous_id: None,
// DEFAULT: Telemetry is OPT-IN, never OPT-OUT
collect_telemetry: Some(false),
editor: Some("nano".to_string()),
}
}
}
}
52 changes: 42 additions & 10 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use utils::agent_context::AgentContext;
use utils::agents_md::discover_agents_md;
use utils::apps_md::discover_apps_md;
use utils::check_update::check_update;
use utils::check_update::{check_update, auto_update};
use utils::gitignore;
use utils::local_context::analyze_local_context;

Expand Down Expand Up @@ -278,8 +278,23 @@ async fn main() {
Cli::parse()
};

if let Some(workdir) = &cli.workdir {
let workdir = Path::new(workdir);
// AUTO-UPDATE: BLOCKED BY DEFAULT - requires explicit OPT-IN
// Only run auto-update if user sets STAKPAK_ENABLE_UPDATES=1 (dangerous)
let should_check_updates = std::env::var("STAKPAK_ENABLE_UPDATES")
.unwrap_or_else(|_| "0".to_string())
.eq_ignore_ascii_case("1");

if should_check_updates
&& cli.command.is_none()
&& !cli.r#async
&& !cli.print
&& let Err(e) = auto_update().await
{
eprintln!("Auto-update failed: {}", e);
}

if let Some(ref workdir) = cli.workdir {
let workdir = Path::new(&workdir);
if let Err(e) = env::set_current_dir(workdir) {
eprintln!("Failed to set current directory: {}", e);
std::process::exit(1);
Expand Down Expand Up @@ -332,17 +347,26 @@ async fn main() {
return; // Exit after warden execution completes
}

if config.machine_name.is_none() {
// Generate a random machine name
// MACHINE FINGERPRINTING: BLOCKED BY DEFAULT - requires explicit OPT-IN
// Only generate machine name if user sets STAKPAK_GENERATE_MACHINE_ID=1 (dangerous)
let should_generate_machine_id = std::env::var("STAKPAK_GENERATE_MACHINE_ID")
.unwrap_or_else(|_| "0".to_string())
.eq_ignore_ascii_case("1");

if should_generate_machine_id && config.machine_name.is_none() {
// Generate a random machine name (user opted-in)
let random_name = names::Generator::with_naming(Name::Numbered)
.next()
.unwrap_or_else(|| "unknown-machine".to_string());

config.machine_name = Some(random_name);

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

// Run interactive/async agent when no subcommand or Init; otherwise run the subcommand
Expand Down Expand Up @@ -417,14 +441,20 @@ async fn main() {
std::process::exit(1);
}));

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

let (api_result, update_result, rulebooks_result) = tokio::join!(
// Check updates sequentially to avoid type inference issues
let update_result = if should_check_updates {
check_update(&current_version).await
} else {
Ok(())
};

let (api_result, rulebooks_result) = tokio::join!(
client.get_my_account(),
check_update(&current_version),
async {
client_for_rulebooks
.list_rulebooks()
Expand Down Expand Up @@ -1106,6 +1136,7 @@ mod tests {
}

#[test]
#[test]
fn autopilot_related_commands_do_not_require_auth() {
assert!(
!Commands::Autopilot(commands::AutopilotCommands::Status {
Expand Down Expand Up @@ -1211,7 +1242,7 @@ mod tests {
fn ak_search_help_explains_recursive_preview_and_filters() {
let result = Cli::try_parse_from(["stakpak", "ak", "search", "--help"]);
let error = match result {
Ok(_) => panic!("help output should exit via clap error"),
Ok(_) => panic!("help output experience should exit via clap error"),
Err(error) => error,
};
let help = error.to_string();
Expand All @@ -1223,4 +1254,5 @@ mod tests {
assert!(help.contains("-i"));
assert!(!help.contains("--json"));
}

}
48 changes: 43 additions & 5 deletions cli/src/onboarding/save_config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Configuration saving utilities

use crate::config::{ConfigFile, ProfileConfig, ProviderType};
use stakpak_shared::telemetry::{TelemetryEvent, capture_event};
use crate::onboarding::config_templates::config_to_toml_preview;
use crate::onboarding::styled_output;
use stakpak_shared::telemetry::{TelemetryEvent, capture_event, is_telemetry_enabled, is_telemetry_env_enabled};
use std::fs;
use std::path::PathBuf;

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

if is_local_provider && config_file.settings.anonymous_id.is_none() {
// SOVEREIGNTY GUARD: Only generate anonymous_id if telemetry is explicitly enabled
// Both config AND environment variable must opt-in
if is_local_provider
&& config_file.settings.anonymous_id.is_none()
&& is_telemetry_enabled(config_file.settings.collect_telemetry)
&& is_telemetry_env_enabled()
{
config_file.settings.anonymous_id = Some(uuid::Uuid::new_v4().to_string());
}

// DO NOT set collect_telemetry to true by default - maintain opt-in only
// If user hasn't explicitly set it, keep it as None (will default to false)
if is_local_provider && config_file.settings.collect_telemetry.is_none() {
config_file.settings.collect_telemetry = Some(true);
// Never auto-enable telemetry - keep it disabled by default
config_file.settings.collect_telemetry = Some(false);
}

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

// SOVEREIGNTY GUARD: Only capture telemetry if user explicitly enabled it
// Requires: config opt-in AND env var opt-in AND anonymous_id generated
if is_local_provider
&& is_first_telemetry_setup
&& is_telemetry_enabled(config_file.settings.collect_telemetry)
&& is_telemetry_env_enabled()
&& config_file.settings.collect_telemetry.unwrap_or(false)
&& let Some(ref anonymous_id) = config_file.settings.anonymous_id
&& config_file.settings.collect_telemetry.unwrap_or(true)
{
capture_event(
anonymous_id,
config_file.settings.machine_name.as_deref(),
true,
true, // telemetry is enabled
TelemetryEvent::FirstOpen,
);
}
Expand All @@ -72,3 +88,25 @@ pub fn save_to_profile(
collect_telemetry: config_file.settings.collect_telemetry,
})
}

/// Show configuration preview and confirm before saving to a named profile
pub fn preview_and_save_to_profile(
config_path: &str,
profile_name: &str,
profile: ProfileConfig,
) -> Result<TelemetrySettings, String> {
// Show preview
styled_output::render_config_preview(&config_to_toml_preview(&profile, profile_name));

// Save
let telemetry_settings = save_to_profile(config_path, profile_name, profile)?;

println!();
styled_output::render_success(&format!(
"Configuration saved successfully to [profiles.{}]",
profile_name
));
println!();

Ok(telemetry_settings)
}
Loading
Loading