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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Environment overrides: `AUTTER_API_BASE_URL`, `AUTTER_WEB_URL`, `AUTTER_NOTES_BA

### Telemetry

Autter reports **anonymous** usage analytics and error data so the tool can improve. Telemetry is **on by default**, is confirmed during `autter onboard`, and is built to be safe and fully auditable:
Autter reports **anonymous** usage analytics and error data so the tool can improve. Telemetry is **off until you opt in** during `autter onboard` (or with `autter telemetry on`), and is built to be safe and fully auditable:

- **Never any personal data** — no code, prompts, file paths, repo names, usernames, or IP addresses.
- **Only coarse, anonymous signals** — a random install ID, device info (OS, CPU architecture, core count), the Autter version, and error/exception messages.
Expand Down
6 changes: 4 additions & 2 deletions data-privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

## OSS Mode

If you install Autter open source and don't login **no code, prompts, or agent usage data is ever sent to Autter**. Autter runs entirely on your machine and writes attribution data into your local git repository and prompts to a local SQLite.
Unless you choose connected mode (`autter onboard --connect`, or "Connected" in `autter onboard`), **no code, prompts, or agent usage data is ever sent to Autter**. Signing in with `autter login` alone does not turn on uploads. Autter runs entirely on your machine and writes attribution data into your local git repository and prompts to a local SQLite. `autter whoami` and `autter sync status` show "Cloud upload: off" exactly when nothing is being uploaded.

The only data Autter sends externally in open source mode is error and exception telemetry, which is enabled by default to help us improve the tool. You can disable or redirect it at any time by turning `telemetry_oss` to `off`. See [configuration options](https://autter.dev/docs/cli/configuration#configuration-options) for details.
Before you finish `autter onboard`, Autter does not read your AI agents' session history, and plain commands such as `autter status` or `autter doctor` do not start the background service.

The only other data Autter can send is anonymous error and usage telemetry. It is off until you opt in during `autter onboard` or with `autter telemetry on`, and you can turn it off at any time by setting `telemetry_oss` to `off`. See [configuration options](https://autter.dev/docs/cli/configuration#configuration-options) for details.

## Data

Expand Down
8 changes: 5 additions & 3 deletions src/auth/notice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,10 @@ pub enum CloudSyncAttention {
DaemonNotRunning,
}

/// Whether cloud upload is on. Same predicate the uploaders check, so the
/// status line can never say "off" while data is being sent.
fn cloud_sync_enabled() -> bool {
let cfg = crate::config::Config::fresh();
cfg.platform_sync_enabled() && cfg.notes_backend_kind().uses_http()
crate::config::platform_sync_enabled_now()
}

fn background_service_running() -> bool {
Expand Down Expand Up @@ -322,7 +323,8 @@ pub struct PendingSyncCountsJson {
/// Machine-readable cloud-sync health for status commands.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct CloudSyncStatusReport {
/// Whether the configured notes backend uploads to autter cloud.
/// Whether this machine uploads to autter cloud (connected mode). Matches
/// what every uploader checks: when false, nothing is sent.
pub enabled: bool,
pub daemon_running: bool,
/// Overall state: healthy, draining a backlog, or blocked.
Expand Down
10 changes: 8 additions & 2 deletions src/commands/autter_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,15 @@ pub fn handle_autter(args: &[String]) {
);
if needs_daemon {
use crate::daemon::telemetry_handle::{
DaemonTelemetryInitResult, init_daemon_telemetry_handle,
DaemonTelemetryInitResult, init_daemon_telemetry_handle_with,
};
match init_daemon_telemetry_handle() {
// Before onboarding, plain commands use a daemon that is already
// running but never start one: the background service is a system
// integration that needs the user's consent. Agent checkpoints come
// from hooks the user already installed, so they may still start it.
let allow_spawn =
args[0].as_str() == "checkpoint" || crate::config::collection_consented_now();
match init_daemon_telemetry_handle_with(allow_spawn) {
DaemonTelemetryInitResult::Connected | DaemonTelemetryInitResult::Skipped => {}
DaemonTelemetryInitResult::Failed(err) => {
eprintln!(
Expand Down
14 changes: 12 additions & 2 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,13 @@ fn run_doctor(options: &DoctorOptions) -> DoctorOutput {
"background service",
crate::diagnostics::ensure_daemon_ready_for_self_checks(),
);
let daemon_ok = daemon_check.status != DoctorStatus::Failed;
// Skipped means the service is intentionally not running (before
// onboarding), so there is nothing to send a git event to.
let daemon_check_skipped = daemon_check.status == DoctorStatus::Skipped;
let daemon_ok = !matches!(
daemon_check.status,
DoctorStatus::Failed | DoctorStatus::Skipped
);
reporter.add(daemon_check);

// Separately: do real git trace2 events reach the service? A failure here
Expand All @@ -176,7 +182,11 @@ fn run_doctor(options: &DoctorOptions) -> DoctorOutput {
reporter.add(skipped_check(
SECTION_SERVICE,
"trace2 ingestion",
"skipped -- fix the background service check first",
if daemon_check_skipped {
"skipped -- the background service starts after `autter onboard`"
} else {
"skipped -- fix the background service check first"
},
));
}

Expand Down
35 changes: 26 additions & 9 deletions src/commands/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ pub fn run_device_login_with(login_ui: LoginUi) -> Result<LoginOutcome, String>
let ctx = crate::api::client::ApiContext::new(None);
if ctx.auth_token.is_some() {
crate::auth::notice::clear_sync_auth_blocked();
eprintln!("Stored credentials still work. Resuming cloud sync.");
resume_cloud_sync_after_login(false);
eprintln!("Stored credentials still work.");
resume_cloud_sync_after_login(false, login_ui.decline);
return Ok(LoginOutcome::LoggedIn);
}
eprintln!("Stored credentials could not authenticate. Starting a new sign-in...\n");
Expand Down Expand Up @@ -162,7 +162,7 @@ pub fn run_device_login_with(login_ui: LoginUi) -> Result<LoginOutcome, String>
caps.paint("1;32", &format!("{ok} CLI authorization successful."))
);
print_login_identity(&creds.access_token);
resume_cloud_sync_after_login(false);
resume_cloud_sync_after_login(false, login_ui.decline);

Ok(LoginOutcome::LoggedIn)
}
Expand Down Expand Up @@ -267,7 +267,7 @@ fn finish_interactive(
&format!("Signed in as {}", identity_summary(&creds.access_token)),
login_ui.counter,
);
resume_cloud_sync_after_login(true);
resume_cloud_sync_after_login(true, login_ui.decline);
Ok(LoginOutcome::LoggedIn)
}

Expand Down Expand Up @@ -330,7 +330,7 @@ pub fn run_pat_login(token: &str) -> Result<LoginOutcome, String> {

eprintln!("Successfully logged in with a Personal Access Token.");
print_login_identity(&creds.access_token);
resume_cloud_sync_after_login(false);
resume_cloud_sync_after_login(false, Decline::Cancel);
Ok(LoginOutcome::LoggedIn)
}

Expand Down Expand Up @@ -382,10 +382,13 @@ fn print_login_identity(access_token: &str) {
/// any in-memory sync backoff. Skipped in test harnesses, which manage their
/// own daemon lifecycle. Best-effort: a failed restart must not fail login.
/// `on_rail` prints the notes under an interactive flow's rail.
fn resume_cloud_sync_after_login(on_rail: bool) {
if std::env::var_os("AUTTER_TEST_DB_PATH").is_some() {
return;
}
///
/// Signing in never turns cloud sync on by itself. Outside connected mode
/// (see [`crate::config::Config::platform_sync_enabled`]) nothing uploads, so
/// say so instead of promising an upload, and leave the service alone.
/// `decline` tells standalone `autter login` (`Cancel`) apart from onboarding
/// (`ContinueLocally`), which picks the mode itself right after sign-in.
fn resume_cloud_sync_after_login(on_rail: bool, decline: Decline) {
let say = |text: &str| {
if on_rail {
rail::print_note(text);
Expand All @@ -394,6 +397,20 @@ fn resume_cloud_sync_after_login(on_rail: bool) {
}
};

if !crate::config::platform_sync_enabled_now() {
// Inside onboarding, `setup_connected` switches to connected mode right
// after this returns, and onboarding restarts the service itself.
if decline == Decline::Cancel {
say("Cloud upload is off: signing in does not upload anything.");
say("To sync this machine with your org, run `autter onboard --connect`.");
}
return;
}

if std::env::var_os("AUTTER_TEST_DB_PATH").is_some() {
return;
}

let pending = crate::auth::notice::pending_sync_counts();
if pending.total() > 0 {
say(&format!(
Expand Down
29 changes: 19 additions & 10 deletions src/commands/onboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ pub fn handle_onboard(args: &[String]) {

let logged_in = matches!(collect_auth_status().state, AuthState::LoggedIn);
let interactive = ui::is_interactive();
let ask_mode = !(choose_connect || choose_local || logged_in);
// Being signed in is not a choice to upload: credentials can come from
// `autter login` or an install nonce without the user ever picking
// connected mode. Only `--connect` or the interactive answer turns sync on.
let ask_mode = !(choose_connect || choose_local);

if ask_mode && !interactive {
// Non-interactive shell with no explicit choice: don't block automated
Expand Down Expand Up @@ -117,12 +120,8 @@ pub fn handle_onboard(args: &[String]) {
true
} else if choose_local {
false
} else if logged_in {
// Already authenticated (e.g. via an install nonce): treat as connected
// without prompting.
true
} else {
prompt_mode_choice(steps.next().as_deref())
prompt_mode_choice(steps.next().as_deref(), logged_in)
};
steps.set_total(plan.total(connect));

Expand All @@ -142,7 +141,12 @@ pub fn handle_onboard(args: &[String]) {
crate::upload_hold::PURGE_COMMAND,
);
}
if let Err(e) = setup_connected(&mut file_config, logged_in, !ask_mode, &mut steps) {
if let Err(e) = setup_connected(
&mut file_config,
logged_in,
!ask_mode || logged_in,
&mut steps,
) {
report.problem(
format!(
"Could not connect to the Autter platform ({e}); set up in local mode instead"
Expand Down Expand Up @@ -338,11 +342,16 @@ fn restart_daemon_for_mode_change() -> Result<(), String> {
}

/// The interactive connected-vs-local choice, as an arrow-key selector.
fn prompt_mode_choice(counter: Option<&str>) -> bool {
fn prompt_mode_choice(counter: Option<&str>, logged_in: bool) -> bool {
let connected_hint = if logged_in {
"Powers prompt search, team dashboards, and PR authorship views. Uses the account you're signed in with."
} else {
"Powers prompt search, team dashboards, and PR authorship views. Sign-in opens in your browser."
};
let items = [
SelectItem::new(
"Connected — link this machine to your Autter org (recommended)",
"Powers prompt search, team dashboards, and PR authorship views. Sign-in opens in your browser.",
connected_hint,
),
SelectItem::new(
"Local only — everything stays on this machine",
Expand Down Expand Up @@ -683,7 +692,7 @@ fn print_status_summary() {

/// Show the current telemetry setting and where to inspect what's sent.
fn print_telemetry_summary(cfg: &config::FileConfig) {
let enabled = cfg.telemetry_oss.as_deref() != Some("off");
let enabled = config::telemetry_oss_opted_in(cfg.telemetry_oss.as_deref());
eprintln!();
if enabled {
eprintln!("Anonymous telemetry is ON. Everything sent is logged at:");
Expand Down
13 changes: 7 additions & 6 deletions src/commands/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ fn log_path() -> Option<std::path::PathBuf> {

fn is_enabled() -> bool {
// Reads the persisted choice directly so it reflects edits made outside this
// process. Telemetry is on unless explicitly set to "off".
config::load_file_config_public()
.ok()
.and_then(|c| c.telemetry_oss)
.as_deref()
!= Some("off")
// process. Telemetry is off until explicitly set to "on".
config::telemetry_oss_opted_in(
config::load_file_config_public()
.ok()
.and_then(|c| c.telemetry_oss)
.as_deref(),
)
}

fn print_status() {
Expand Down
Loading
Loading