openlatch-client 0.1.7

The open-source security layer for AI agents — client forwarder
//! `openlatch telemetry` subcommand handlers.
//!
//! Surface: `status`, `enable`, `disable`, `purge`, `debug`. Each command is
//! idempotent and deliberately prints plain-English output that explains
//! which rule decided the current state (invariant I7 — observable state).

use crate::cli::output::{OutputConfig, OutputFormat};
use crate::cli::TelemetryCommands;
use crate::config;
use crate::error::OlError;
use crate::telemetry::{
    config as tconfig,
    consent::{resolve, DecidedBy},
    consent_file_path,
};

/// Dispatch for `openlatch telemetry <subcommand>`.
pub fn run(cmd: &TelemetryCommands, output: &OutputConfig) -> Result<(), OlError> {
    match cmd {
        TelemetryCommands::Status => run_status(output),
        TelemetryCommands::Enable => run_enable(output),
        TelemetryCommands::Disable => run_disable(output),
        TelemetryCommands::Purge => run_purge(output),
        TelemetryCommands::Debug => run_debug(output),
    }
}

fn run_status(output: &OutputConfig) -> Result<(), OlError> {
    let dir = config::openlatch_dir();
    let path = consent_file_path(&dir);
    let resolved = resolve(&path);
    let rule = decided_by_label(resolved.decided_by);
    let build_includes = crate::telemetry::build_includes_telemetry();

    match output.format {
        OutputFormat::Json => {
            let json = serde_json::json!({
                "enabled": resolved.enabled(),
                "decided_by": rule,
                "build_includes_telemetry": build_includes,
                "consent_file": path.to_string_lossy(),
            });
            output.print_json(&json);
        }
        OutputFormat::Human => {
            if output.quiet {
                return Ok(());
            }
            let state = if resolved.enabled() {
                "enabled"
            } else {
                "disabled"
            };
            println!("telemetry: {state}");
            println!("  decided by: {rule}");
            println!("  build includes telemetry: {build_includes}");
            println!("  consent file: {}", path.display());
            if !resolved.enabled() {
                println!();
                println!("To turn on: openlatch telemetry enable");
            }
        }
    }
    Ok(())
}

fn run_enable(output: &OutputConfig) -> Result<(), OlError> {
    let dir = config::openlatch_dir();
    std::fs::create_dir_all(&dir).map_err(|e| {
        OlError::new(
            crate::error::ERR_TELEMETRY_WRITE_FAILED,
            format!("cannot create openlatch directory: {e}"),
        )
    })?;
    let path = consent_file_path(&dir);
    tconfig::write_consent(&path, true)?;
    if output.format == OutputFormat::Json {
        output.print_json(&serde_json::json!({ "enabled": true }));
    } else if !output.quiet {
        println!("telemetry enabled — thanks for helping shape OpenLatch.");
        println!("disable anytime with: openlatch telemetry disable");
    }
    Ok(())
}

fn run_disable(output: &OutputConfig) -> Result<(), OlError> {
    let dir = config::openlatch_dir();
    std::fs::create_dir_all(&dir).map_err(|e| {
        OlError::new(
            crate::error::ERR_TELEMETRY_WRITE_FAILED,
            format!("cannot create openlatch directory: {e}"),
        )
    })?;
    let path = consent_file_path(&dir);
    tconfig::write_consent(&path, false)?;
    if output.format == OutputFormat::Json {
        output.print_json(&serde_json::json!({ "enabled": false }));
    } else if !output.quiet {
        println!("telemetry disabled.");
    }
    Ok(())
}

fn run_purge(output: &OutputConfig) -> Result<(), OlError> {
    // Purge = disable + signal running daemon to stop capturing.
    // Phase A has no daemon IPC for the purge signal yet; disabling the
    // file is sufficient for new invocations. Task 2 wires the running
    // daemon to observe the file and call `TelemetryHandle::disable_now`.
    run_disable(output)?;
    if output.format != OutputFormat::Json && !output.quiet {
        println!("in-memory queue will be dropped on next daemon restart.");
    }
    Ok(())
}

fn run_debug(output: &OutputConfig) -> Result<(), OlError> {
    if output.format == OutputFormat::Json {
        output.print_json(&serde_json::json!({
            "hint": "run any openlatch command with OPENLATCH_TELEMETRY_DEBUG=1 to log event envelopes to stderr"
        }));
    } else if !output.quiet {
        println!("telemetry debug:");
        println!("  set OPENLATCH_TELEMETRY_DEBUG=1 before any openlatch command");
        println!("  every event that would be captured is printed to stderr as JSON");
        println!("  no network is used; debug mode is orthogonal to consent");
    }
    Ok(())
}

fn decided_by_label(d: DecidedBy) -> &'static str {
    match d {
        DecidedBy::DoNotTrackEnv => "DO_NOT_TRACK env var",
        DecidedBy::OpenlatchDisabledEnv => "OPENLATCH_TELEMETRY_DISABLED env var",
        DecidedBy::CiEnvironment => "CI environment detected",
        DecidedBy::ConfigFile => "telemetry.json",
        DecidedBy::DefaultUnconsented => "default (no consent recorded yet)",
        DecidedBy::NoBakedKey => "no baked PostHog key in this build",
    }
}