use crate::auth::{retrieve_credential, FileCredentialStore, KeyringCredentialStore};
use crate::cli::commands::lifecycle;
use crate::cli::commands::proxy;
use crate::cli::output::{OutputConfig, OutputFormat};
use crate::cli::AuthLoginArgs;
use crate::cli::InitArgs;
use crate::config;
use crate::error::{OlError, ERR_INVALID_CONFIG, ERR_PORT_IN_USE};
use crate::hooks;
use crate::hooks::DetectedAgent;
use crate::telemetry::{self, config as telemetry_config, consent_file_path, Event};
use secrecy::ExposeSecret;
use std::io::{BufRead, Write};
pub fn run_init(args: &InitArgs, output: &OutputConfig) -> Result<(), OlError> {
crate::cli::header::print(output, &["init"]);
if args.dry_run {
return run_dry_run(args, output);
}
let mut ledger = InitLedger::default();
let ol_dir = config::openlatch_dir();
ledger.create_dir(&ol_dir).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!(
"Cannot create openlatch directory '{}': {e}",
ol_dir.display()
),
)
.with_suggestion("Check that you have write permission to your home directory.")
})?;
ledger.create_dir(&ol_dir.join("logs")).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot create logs directory: {e}"),
)
})?;
let config_path = config::openlatch_dir().join("config.toml");
let re_init = config_path.exists();
let re_init_report = if re_init {
run_egress_gate(args, args.api_url.as_deref(), output)?
} else {
GateReport::default()
};
let reclaim = {
let pre_cfg = config::Config::load(None, None, false).unwrap_or_else(|_| {
let mut c = config::Config::defaults();
c.port = config::read_port_file().unwrap_or(c.port);
c
});
match lifecycle::reclaim_ports(&pre_cfg, output) {
Ok(outcome) => {
match outcome.action {
lifecycle::ReclaimAction::Nothing => {
output.print_step("No prior daemon to reclaim")
}
lifecycle::ReclaimAction::Stopped => output.print_step(&format!(
"Reclaimed daemon ({})",
outcome.identity.describe()
)),
lifecycle::ReclaimAction::ForceKilled => output.print_step(&format!(
"Force-killed unresponsive daemon ({})",
outcome.identity.describe()
)),
}
outcome
}
Err(e) => {
output.print_error(&e);
return Err(e);
}
}
};
let agents = hooks::detect_agents();
let agents = match hooks::select_agents(agents, &args.agent) {
Ok(v) => v,
Err(e) => {
output.print_error(&e);
return Err(e);
}
};
if agents.is_empty() {
let e = hooks::agent_not_found_err();
output.print_error(&e);
return Err(e);
}
for a in &agents {
output.print_step(&format!("Detected agent: {}", agent_label(a)));
}
let token_path = ol_dir.join("daemon.token");
let token_existed = token_path.exists();
let new_token = config::generate_token();
if !token_existed {
ledger.record_file(&token_path);
}
std::fs::write(&token_path, &new_token).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot write token file '{}': {e}", token_path.display()),
)
.with_suggestion("Check that you have write permission to the openlatch directory.")
})?;
crate::fs_secure::restrict_to_owner(&token_path).map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Cannot set permissions on token file: {e}"),
)
})?;
let token_action = if token_existed {
"(regenerated existing)"
} else {
"(new)"
};
output.print_step(&format!("Generated auth token {token_action}"));
let needs_port_probe = !config_path.exists() || args.reconfig;
let port = if needs_port_probe {
if args.reconfig {
let _ = lifecycle::run_stop(output);
let _ = std::fs::remove_file(config::openlatch_dir().join("daemon.port"));
let _ = std::fs::remove_file(&config_path);
}
let requested = std::env::var("OPENLATCH_PORT")
.ok()
.filter(|v| !v.trim().is_empty())
.map(|v| config::parse_port_env(&v))
.transpose()?;
let selected = match requested {
Some(p) => {
if std::net::TcpListener::bind(("127.0.0.1", p)).is_err() {
return Err(OlError::new(
ERR_PORT_IN_USE,
format!("OPENLATCH_PORT={p} is already in use"),
)
.with_suggestion(format!(
"Free port {p}, choose another via OPENLATCH_PORT, or unset it to probe {}-{} automatically.",
config::PORT_RANGE_START,
config::PORT_RANGE_END
))
.with_docs("https://docs.openlatch.ai/errors/OL-1500"));
}
output.print_substep(&format!("Selected port {p} (from OPENLATCH_PORT)"));
p
}
None => {
let probed =
config::probe_free_port(config::PORT_RANGE_START, config::PORT_RANGE_END)?;
output.print_substep(&format!("Selected port {probed} (first available)"));
probed
}
};
let config_existed = config_path.exists();
config::ensure_config(selected)?;
if !config_existed {
ledger.record_file(&config_path);
}
let port_file = config::openlatch_dir().join("daemon.port");
let port_file_existed = port_file.exists();
config::write_port_file(selected)?;
if !port_file_existed {
ledger.record_file(&port_file);
}
selected
} else {
config::Config::load(None, None, false)?.port
};
config::ensure_agent_id(&config_path)?;
if let Some(api_url) = &args.api_url {
config::persist_api_url(&config_path, api_url)?;
output.print_substep(&format!("Cloud API URL set to {api_url}"));
}
if args.no_boundary {
config::persist_boundary_enabled(&config_path, false)?;
output.print_substep(
"Model boundary disabled in config — agents connect to the provider directly",
);
}
let cfg = config::Config::load(Some(port), None, false)?;
let gate_report = if re_init {
re_init_report
} else {
match run_egress_gate(args, args.api_url.as_deref(), output) {
Ok(report) => report,
Err(e) => {
ledger.unwind(output);
return Err(e);
}
}
};
let (auth_success, org_name) = run_auth_for_init(output)?;
handle_telemetry_consent(args, output, &ol_dir)?;
match hooks::staging::stage_hook_binary(&ol_dir) {
Ok(outcome) => {
output.print_step(&format!("Hook binary at {}", outcome.target().display()));
}
Err(e) => {
output.print_error(&e);
return Err(e);
}
}
let mut installed: Vec<(&hooks::DetectedAgent, hooks::HookInstallResult)> = Vec::new();
let mut install_failures: Vec<OlError> = Vec::new();
for a in &agents {
match hooks::install_hooks(&*a.binding, cfg.port, &new_token) {
Ok(result) => {
output.print_step(&format!("Hooks written to {}", a.settings_path().display()));
for entry in &result.entries {
let action_label = match entry.action {
hooks::HookAction::Added => "added",
hooks::HookAction::Replaced => "replaced",
};
output.print_substep(&format!("{} ({})", entry.event_type, action_label));
}
installed.push((a, result));
}
Err(e) => {
output.print_info(&format!(
"Warning: could not write hooks for {}: {} ({})",
a.display_name(),
e.message,
e.code
));
install_failures.push(e);
}
}
}
if installed.is_empty() {
let e = install_failures
.into_iter()
.next()
.unwrap_or_else(hooks::agent_not_found_err);
output.print_error(&e);
return Err(e);
}
let (supervision_backend_label, supervision_mode_label, supervision_deferred_reason) =
run_supervision_install_for_init(args, &config_path, output);
let start_plan = plan_daemon_start(
args.no_start,
args.foreground,
supervision_mode_label == "active",
);
let (port, pid) = if start_plan == DaemonStartPlan::Skip {
output.print_step("Skipped daemon start (--no-start)");
(cfg.port, 0u32)
} else if start_plan == DaemonStartPlan::SupervisorOwned {
match lifecycle::verify_running_daemon(cfg.port, 10) {
Ok(pid) => {
output.print_step(&format!(
"Daemon started on port {} (PID {pid}, supervised, v{})",
cfg.port,
env!("OPENLATCH_VERSION")
));
(cfg.port, pid)
}
Err(lifecycle::StartFailure::VersionMismatch { serving, expected }) => {
let e = lifecycle::start_failure_error(
lifecycle::StartFailure::VersionMismatch { serving, expected },
cfg.port,
);
output.print_error(&e);
return Err(e);
}
Err(_)
if lifecycle::read_pid_file()
.filter(|p| lifecycle::is_process_alive(*p))
.is_some() =>
{
let pid = lifecycle::read_pid_file().unwrap_or(0);
output.print_step(&format!(
"Daemon starting under supervision (PID {pid}) — not yet answering /health"
));
if output.format == OutputFormat::Human && !output.quiet {
eprintln!(" Check `openlatch status` shortly, or the newest ~/.openlatch/logs/daemon.log.<date>.");
}
(cfg.port, pid)
}
Err(_) => {
tracing::warn!(
"supervision reported active but no daemon came up; starting one directly"
);
start_and_prove(cfg.port, &new_token, output)?
}
}
} else if args.foreground {
output.print_step(&format!(
"Starting daemon on port {} (foreground)",
cfg.port
));
#[cfg(feature = "boundary")]
let spawn_boundary = cfg.boundary.enabled;
#[cfg(not(feature = "boundary"))]
let spawn_boundary = false;
run_daemon_foreground(cfg.port, &new_token, spawn_boundary)?;
(cfg.port, std::process::id())
} else {
start_and_prove(cfg.port, &new_token, output)?
};
#[cfg(feature = "boundary")]
verify_boundary_preflight(args, &cfg, start_plan, output)?;
if auth_success {
let cloud_msg = if org_name.is_empty() {
"Cloud sync: enabled".to_string()
} else {
format!("Cloud sync: connected (org: {org_name})")
};
output.print_step(&cloud_msg);
if output.format == OutputFormat::Human && !output.quiet {
eprintln!(" Events will be forwarded automatically");
}
}
#[cfg(feature = "boundary")]
if !cfg.boundary.enabled && !args.no_boundary {
warn_boundary_disabled_in_config(&config_path, output);
}
let install_report = build_install_report(args, output);
let today = chrono::Local::now().format("%Y-%m-%d");
let log_path = config::openlatch_dir()
.join("logs")
.join(format!("events-{today}.jsonl"));
let fully_live = install_report
.as_ref()
.map(|r| r.overall() == crate::cli::report::Overall::Healthy)
.unwrap_or(false);
if output.format == OutputFormat::Human && !output.quiet {
eprintln!();
if let Some(report) = &install_report {
report.render(output);
}
if args.no_start {
eprintln!("Setup complete. Run `openlatch start` to launch the daemon.");
} else {
eprintln!("Ready. Events will appear in: {}", log_path.display());
}
if fully_live {
print_init_success_banner(output.color);
}
}
if let Some(report) = &install_report {
report.record_verdict();
}
for (a, result) in &installed {
telemetry::capture_global(Event::cli_initialized(
a.agent_type(),
result.entries.len(),
!token_existed,
));
}
telemetry::capture_global(Event::supervision_installed(
supervision_backend_label,
supervision_mode_label,
supervision_deferred_reason.as_deref(),
));
if output.format == OutputFormat::Json {
let agents_json: Vec<serde_json::Value> = installed
.iter()
.map(|(a, result)| {
serde_json::json!({
"agent": a.agent_type(),
"settings_path": a.settings_path().to_string_lossy(),
"entries": result.entries.len(),
"events": result
.entries
.iter()
.map(|e| e.event_type.as_str())
.collect::<Vec<_>>(),
})
})
.collect();
let cloud_status = if auth_success {
"connected"
} else {
"not_configured"
};
let json = serde_json::json!({
"status": install_report
.as_ref()
.map(|r| r.overall().key())
.unwrap_or("unknown"),
"exit_code": install_report.as_ref().map(|r| r.exit_code()).unwrap_or(0),
"reclaimed": {
"action": match reclaim.action {
lifecycle::ReclaimAction::Nothing => "nothing",
lifecycle::ReclaimAction::Stopped => "stopped",
lifecycle::ReclaimAction::ForceKilled => "force_killed",
},
"pid": reclaim.identity.pid,
"version": reclaim.identity.version,
"uptime_secs": reclaim.identity.uptime_secs,
"exe": reclaim.identity.exe,
},
"report": install_report.as_ref().map(crate::cli::report::Report::to_json),
"agents": agents_json,
"port": port,
"pid": pid,
"log_path": log_path.to_string_lossy(),
"token_action": token_action,
"daemon_started": !args.no_start,
"cloud_status": cloud_status,
"org_name": org_name,
"supervision": {
"mode": supervision_mode_label,
"backend": supervision_backend_label,
"disabled_reason": supervision_deferred_reason,
},
"proxy": {
"source": gate_report.source,
"url_masked": gate_report.url_masked,
"prompted": gate_report.prompted,
},
});
output.print_json(&json);
}
Ok(())
}
#[derive(Debug, Clone)]
enum InitArtifact {
File(std::path::PathBuf),
Directory(std::path::PathBuf),
}
#[derive(Default)]
struct InitLedger {
created: Vec<InitArtifact>,
}
impl InitLedger {
fn record_file(&mut self, path: &std::path::Path) {
self.created.push(InitArtifact::File(path.to_path_buf()));
}
fn create_dir(&mut self, path: &std::path::Path) -> std::io::Result<()> {
let existed = path.exists();
std::fs::create_dir_all(path)?;
if !existed {
self.created
.push(InitArtifact::Directory(path.to_path_buf()));
}
Ok(())
}
fn unwind(&self, output: &OutputConfig) {
if self.created.is_empty() {
return;
}
let mut dirs = Vec::new();
for artifact in &self.created {
match artifact {
InitArtifact::File(p) => {
let _ = std::fs::remove_file(p);
}
InitArtifact::Directory(p) => dirs.push(p),
}
}
dirs.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
for dir in dirs {
let _ = std::fs::remove_dir(dir);
}
output.print_substep("Rolled back — this host is as `init` found it");
}
}
fn run_dry_run(args: &InitArgs, output: &OutputConfig) -> Result<(), OlError> {
let config_path = config::openlatch_dir().join("config.toml");
let cfg = config::Config::load(None, None, false)?;
let api_url = args
.api_url
.clone()
.unwrap_or_else(|| cfg.cloud.api_url.clone());
let overrides = proxy::ProxyOverrides::from_init(args);
overrides.validate()?;
let mut egress_cfg = cfg.egress.clone();
overrides.apply(&mut egress_cfg)?;
proxy::refuse_linux_pac(&egress_cfg)?;
let persisted_source = proxy::PersistedProxy::read(&config_path).source;
let probe_ok = probe_current_route(&api_url, &egress_cfg);
let (action, source) = if persisted_source.as_deref() == Some("manual") {
("keep", persisted_source.clone())
} else if probe_ok {
match &persisted_source {
Some(s) => ("keep", Some(s.clone())),
None if egress_cfg.has_proxy() => ("persist", Some("env".to_string())),
None => ("keep", None),
}
} else {
("discover", None)
};
if output.format == OutputFormat::Json {
let mut proxy_doc = serde_json::json!({ "action": action });
if let Some(s) = &source {
proxy_doc["source"] = serde_json::json!(s);
}
output.print_json(&serde_json::json!({
"status": "ok",
"dry_run": true,
"api_url": api_url,
"proxy": proxy_doc,
}));
} else {
output.print_step("Dry run — nothing was written");
output.print_substep(&format!("api_url {api_url}"));
match &source {
Some(s) => output.print_substep(&format!("proxy {action} (source: {s})")),
None => output.print_substep(&format!("proxy {action}")),
}
}
Ok(())
}
fn probe_current_route(api_url: &str, egress_cfg: &crate::egress::EgressConfig) -> bool {
use crate::egress::CandidateProbe;
let Ok(probe) = crate::egress::HealthProbe::new(api_url, egress_cfg.clone()) else {
return false;
};
let via = egress_cfg
.url
.as_deref()
.filter(|_| egress_cfg.mode != crate::egress::ProxyMode::Direct)
.and_then(|u| reqwest::Url::parse(u).ok());
probe.probe(via.as_ref()).is_ok()
}
#[derive(Debug, Clone, Default)]
struct GateReport {
source: Option<String>,
url_masked: Option<String>,
prompted: bool,
}
fn run_egress_gate(
args: &InitArgs,
api_url_override: Option<&str>,
output: &OutputConfig,
) -> Result<GateReport, OlError> {
let cfg = config::Config::load(None, None, false)?;
let api_url = api_url_override
.map(str::to_string)
.unwrap_or_else(|| cfg.cloud.api_url.clone());
let overrides = proxy::ProxyOverrides::from_init(args);
overrides.validate()?;
let base = cfg.egress.clone();
let mut terminal = crate::cli::prompt::TerminalPrompter::new(api_url.clone());
let prompter: Option<&mut dyn crate::cli::prompt::Prompter> =
if crate::cli::prompt::interactive(output, args.yes) {
Some(&mut terminal)
} else {
None
};
let config_path = config::openlatch_dir().join("config.toml");
let persisted = proxy::PersistedProxy::read(&config_path);
let outcome = match proxy::run_gate(&api_url, base, &overrides, &persisted, prompter, output) {
Ok(o) => o,
Err(failure) => {
report_gate_failure(&failure, &api_url, args, output);
return Err(failure.error);
}
};
let has_writes = !outcome.sets.is_empty() || !outcome.removes.is_empty();
if has_writes && config_path.exists() {
proxy::persist_outcome(&config_path, &outcome, output)?;
}
let probed = outcome.attempts.iter().filter(|a| a.was_probed()).count();
proxy::emit_proxy_configured(&outcome.config, probed, None);
let route = outcome
.config
.url
.as_deref()
.map(crate::egress::mask_userinfo);
match (outcome.source_str(), &route) {
(Some(source), Some(url)) => output.print_step(&format!("proxy via {url} ({source})")),
(Some(source), None) => {
output.print_step(&format!("Cloud reachable (proxy source: {source})"));
}
(None, _) => output.print_step("Cloud reachable (direct)"),
}
Ok(GateReport {
source: outcome.source_str().map(str::to_string),
url_masked: route,
prompted: outcome.prompted,
})
}
fn report_gate_failure(
failure: &proxy::GateFailure,
api_url: &str,
args: &InitArgs,
output: &OutputConfig,
) {
let headless = !crate::cli::prompt::interactive(output, args.yes);
let hint = if headless {
"No terminal to prompt on (or `--yes` was passed), so no proxy was requested. Set one \
with `openlatch init --proxy <url>` or `openlatch proxy set <url>`, or run \
`openlatch init` interactively without `--yes`."
.to_string()
} else {
failure.error.suggestion.clone().unwrap_or_default()
};
if output.format == OutputFormat::Json {
output.print_json(&serde_json::json!({
"status": "failed",
"exit_code": 1,
"api_url": api_url,
"error": { "code": failure.error.code, "message": failure.error.message },
"message": hint,
"candidates": proxy::candidates_json(&failure.attempts),
}));
return;
}
output.print_error(&failure.error);
if headless {
eprintln!(" {hint}");
}
for attempt in &failure.attempts {
if attempt.was_probed() {
eprintln!(" {}", attempt.trace_line());
}
}
}
fn run_auth_for_init(output: &OutputConfig) -> Result<(bool, String), OlError> {
let keyring = KeyringCredentialStore::new();
let cfg = config::Config::load(None, None, false).ok();
let agent_id = cfg
.as_ref()
.and_then(|c| c.agent_id.clone())
.unwrap_or_default();
let file_store =
FileCredentialStore::new(config::openlatch_dir().join("credentials.enc"), agent_id);
let api_url = cfg
.as_ref()
.map(|c| c.cloud.api_url.clone())
.unwrap_or_else(|| "https://app.openlatch.ai".to_string());
let egress = cfg
.as_ref()
.map(|c| c.egress.clone())
.unwrap_or_else(crate::egress::EgressConfig::direct);
let rt = tokio::runtime::Runtime::new().map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to create async runtime: {e}"),
)
})?;
if let Ok(val) = std::env::var("OPENLATCH_API_KEY") {
if !val.is_empty() {
let (online, org_name, _org_id) = rt.block_on(
crate::cli::commands::auth::validate_online(&val, &api_url, &egress),
);
if online {
let msg = if org_name.is_empty() {
"Authenticated via env var".to_string()
} else {
format!("Authenticated via env var (org: {org_name})")
};
output.print_step(&msg);
return Ok((true, org_name));
}
output.print_step("Authenticated via env var (cloud offline - validation skipped)");
return Ok((true, String::new()));
}
}
if let Ok(existing_key) = retrieve_credential(&keyring, &file_store) {
let key_str = existing_key.expose_secret().to_string();
let v = rt.block_on(crate::cli::commands::auth::validate_online_full(
&key_str, &api_url, &egress,
));
if v.online {
let msg = if v.org_name.is_empty() {
"Authenticated".to_string()
} else {
format!("Authenticated (org: {})", v.org_name)
};
output.print_step(&msg);
return Ok((true, v.org_name));
}
if !v.rejected {
output.print_step("Authenticated (cloud offline - using stored credentials)");
return Ok((true, String::new()));
}
output.print_substep("Existing credentials invalid, re-authenticating...");
}
let login_args = AuthLoginArgs { no_browser: false };
crate::cli::commands::auth::run_login(&login_args, output)?;
if let Ok(key) = retrieve_credential(&keyring, &file_store) {
let key_str = key.expose_secret().to_string();
let (_, org_name, _) = rt.block_on(crate::cli::commands::auth::validate_online(
&key_str, &api_url, &egress,
));
return Ok((true, org_name));
}
Ok((true, String::new()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DaemonStartPlan {
Skip,
Foreground,
SupervisorOwned,
SpawnBackground,
}
fn start_and_prove(port: u16, token: &str, output: &OutputConfig) -> Result<(u16, u32), OlError> {
let mut spawned = match lifecycle::spawn_daemon_tracked(port, token) {
Ok(s) => s,
Err(e) => {
output.print_error(&e);
return Err(e);
}
};
match lifecycle::verify_started_daemon(&mut spawned, port, 10) {
Ok(()) => {
output.print_step(&format!(
"Daemon started on port {port} (PID {}, v{})",
spawned.pid,
env!("OPENLATCH_VERSION")
));
Ok((port, spawned.pid))
}
Err(failure) => {
let e = lifecycle::start_failure_error(failure, port);
output.print_error(&e);
Err(e)
}
}
}
pub(crate) fn plan_daemon_start(
no_start: bool,
foreground: bool,
supervision_active: bool,
) -> DaemonStartPlan {
if no_start {
DaemonStartPlan::Skip
} else if foreground {
DaemonStartPlan::Foreground
} else if supervision_active {
DaemonStartPlan::SupervisorOwned
} else {
DaemonStartPlan::SpawnBackground
}
}
fn run_supervision_install_for_init(
args: &InitArgs,
config_path: &std::path::Path,
output: &OutputConfig,
) -> (&'static str, &'static str, Option<String>) {
use crate::supervision::{select_supervisor, SupervisionMode, SupervisorKind};
let skip_reason: Option<&'static str> = if args.foreground {
Some("foreground_session")
} else if args.no_start {
Some("no_start")
} else if args.no_persistence {
Some("user_opt_out")
} else if !crate::supervision::unreproducible_environment().is_empty() {
Some("isolated_instance")
} else {
None
};
if let Some(reason) = skip_reason {
let _ = config::persist_supervision_state(
config_path,
&SupervisionMode::Disabled,
&SupervisorKind::None,
Some(reason),
);
let msg = match reason {
"user_opt_out" => "Supervision: skipped (--no-persistence)",
"isolated_instance" => {
"Supervision: not applicable — an isolated instance is not machine-global"
}
"foreground_session" => "Supervision: skipped (foreground session)",
"no_start" => "Supervision: skipped (--no-start)",
_ => "Supervision: skipped",
};
output.print_step(msg);
return ("none", "disabled", Some(reason.to_string()));
}
let Some(supervisor) = select_supervisor() else {
let reason = "unsupported_os";
let _ = config::persist_supervision_state(
config_path,
&SupervisionMode::Deferred,
&SupervisorKind::None,
Some(reason),
);
output
.print_step("Supervision: deferred (no supported supervisor detected on this system)");
return ("none", "deferred", Some(reason.to_string()));
};
let exe_path =
std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("openlatch"));
let backend = supervisor.kind();
let backend_label: &'static str = match backend {
SupervisorKind::Launchd => "launchd",
SupervisorKind::Systemd => "systemd",
SupervisorKind::TaskScheduler => "task_scheduler",
SupervisorKind::None => "none",
};
match supervisor.install(&exe_path) {
Ok(()) => {
let _ = config::persist_supervision_state(
config_path,
&SupervisionMode::Active,
&backend,
None,
);
output.print_step(&format!(
"Supervision installed ({backend_label}) — daemon will auto-start on login"
));
if output.format == OutputFormat::Human && !output.quiet {
eprintln!(
" Disable with `openlatch supervision disable` or run `openlatch init --no-persistence`."
);
}
(backend_label, "active", None)
}
Err(e) => {
let reason_text = format!("{} ({})", e.message, e.code);
let _ = config::persist_supervision_state(
config_path,
&SupervisionMode::Deferred,
&backend,
Some(&reason_text),
);
output.print_step(&format!(
"Supervision: deferred — {backend_label} install failed ({})",
e.code
));
if output.format == OutputFormat::Human && !output.quiet {
eprintln!(" {}", e.message);
eprintln!(
" Init will continue; run `openlatch supervision install` to retry after the issue is resolved."
);
}
(backend_label, "deferred", Some(reason_text))
}
}
}
fn agent_label(agent: &DetectedAgent) -> String {
format!(
"{} ({})",
agent.display_name(),
agent.config_dir().display()
)
}
fn run_daemon_foreground(port: u16, token: &str, spawn_boundary: bool) -> Result<(), OlError> {
let mut cfg = config::Config::load(Some(port), None, true)?;
cfg.foreground = true;
let rt = tokio::runtime::Runtime::new().map_err(|e| {
OlError::new(
ERR_INVALID_CONFIG,
format!("Failed to create async runtime: {e}"),
)
})?;
let token_owned = token.to_string();
let pid = std::process::id();
#[cfg(feature = "crash-report")]
crate::crash_report::enrich_daemon_scope(cfg.port, pid);
rt.block_on(async move {
use crate::envelope;
use crate::logging;
use crate::privacy;
let _guard = logging::daemon_log::init_daemon_logging(&cfg.log_dir, true);
if let Ok(deleted) = logging::cleanup_old_logs(&cfg.log_dir, cfg.retention_days) {
if deleted > 0 {
tracing::info!(deleted = deleted, "cleaned up old log files");
}
}
privacy::init_filter(&cfg.extra_patterns);
let pid_path = config::openlatch_dir().join("daemon.pid");
if let Err(e) = std::fs::write(&pid_path, pid.to_string()) {
tracing::warn!(error = %e, "failed to write PID file");
}
logging::daemon_log::log_startup(
env!("CARGO_PKG_VERSION"),
cfg.port,
pid,
envelope::os_string(),
envelope::arch_string(),
);
crate::cli::commands::lifecycle::log_observability_status_from_env();
let credential_store = crate::cli::commands::lifecycle::build_credential_store();
match crate::daemon::start_server(
cfg.clone(),
token_owned,
Some(credential_store),
spawn_boundary,
)
.await
{
Ok((uptime_secs, events)) => {
eprintln!(
"openlatch daemon stopped \u{2022} uptime {} \u{2022} {} events processed",
crate::daemon::format_uptime(uptime_secs),
events
);
}
Err(e) => {
tracing::error!(error = %e, "daemon exited with error");
eprintln!("Error: daemon exited unexpectedly: {e}");
}
}
let _ = std::fs::remove_file(&pid_path);
});
#[cfg(feature = "crash-report")]
crate::crash_report::flush(std::time::Duration::from_secs(2));
Ok(())
}
fn handle_telemetry_consent(
args: &InitArgs,
output: &OutputConfig,
ol_dir: &std::path::Path,
) -> Result<(), OlError> {
let consent_path = consent_file_path(ol_dir);
if args.no_telemetry {
telemetry_config::write_consent(&consent_path, false)?;
output.print_step("Telemetry: disabled (--no-telemetry)");
return Ok(());
}
if args.telemetry {
telemetry_config::write_consent(&consent_path, true)?;
output.print_step("Telemetry: enabled (--telemetry)");
return Ok(());
}
if consent_path.exists() {
return Ok(());
}
let interactive = crate::cli::prompt::interactive(output, args.yes);
if !interactive {
telemetry_config::write_consent(&consent_path, false)?;
if output.format == OutputFormat::Human && !output.quiet {
eprintln!(
"ℹ Telemetry is off in non-interactive mode. Enable with `openlatch telemetry enable`."
);
}
return Ok(());
}
print_telemetry_notice();
let enabled = read_consent_answer();
telemetry_config::write_consent(&consent_path, enabled)?;
if enabled {
output.print_step("Telemetry: enabled — thanks for helping shape OpenLatch.");
} else {
output.print_step("Telemetry: disabled — enable later with `openlatch telemetry enable`.");
}
Ok(())
}
fn print_init_success_banner(color: bool) {
use crate::cli::color as c;
let check = c::checkmark(color);
let headline = c::bold(
"OpenLatch is now capturing activity from your AI agents",
color,
);
let url = c::bold("https://app.openlatch.ai", color);
let rule = c::dim(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
color,
);
let lines = [
String::new(),
rule.clone(),
format!(" {check} {headline}"),
String::new(),
" Nothing else to do — OpenLatch runs in the background,".to_string(),
" capturing what your agents do so you can see and".to_string(),
" control it from the console.".to_string(),
String::new(),
" Explore agent activity, inventory, and the audit trail:".to_string(),
format!(" {url}"),
rule,
];
for line in lines {
eprintln!("{line}");
}
let _ = std::io::stderr().flush();
}
fn print_telemetry_notice() {
let lines = [
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
" Help shape OpenLatch",
"",
" OpenLatch is early, and anonymous usage data is how we",
" learn which agents, detections, and workflows matter",
" most to the people we protect.",
"",
" What we collect:",
" ✓ Command names (e.g. `init`, `status`) and durations",
" ✓ Which AI agents you use",
" ✓ Error codes and daemon health signals",
" ✓ Aggregated hook volume (counts only, every 5 min)",
"",
" What we NEVER collect:",
" ✗ File contents, source code, or prompts",
" ✗ Environment variables, tokens, or secrets",
" ✗ Command arguments or flag values",
" ✗ IP addresses, hostnames, usernames",
"",
" Turn it off anytime: openlatch telemetry disable",
"",
" Share anonymous usage data to help improve OpenLatch? [Y/n]",
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
];
for line in lines {
eprintln!("{line}");
}
let _ = std::io::stderr().flush();
}
fn read_consent_answer() -> bool {
let mut buf = String::new();
let stdin = std::io::stdin();
let _ = stdin.lock().read_line(&mut buf);
let answer = buf.trim().to_ascii_lowercase();
!matches!(answer.as_str(), "n" | "no")
}
#[cfg(feature = "boundary")]
const PREFLIGHT_VERDICT_WAIT: std::time::Duration = std::time::Duration::from_secs(15);
#[cfg(feature = "boundary")]
fn verify_boundary_preflight(
args: &InitArgs,
cfg: &config::Config,
start_plan: DaemonStartPlan,
output: &OutputConfig,
) -> Result<(), OlError> {
if start_plan == DaemonStartPlan::Skip
|| args.foreground
|| !cfg.boundary.enabled
|| args.no_boundary
|| !cfg.boundary.owns_agent_wiring()
{
return Ok(());
}
let planes: Vec<(&'static str, crate::boundary::wire_format::WireFormat)> =
crate::hooks::detect_agents()
.iter()
.filter_map(|a| {
a.binding
.boundary_wiring()
.map(|w| (a.agent_type(), w.wire_format))
})
.collect();
if planes.is_empty() {
return Ok(());
}
let port = cfg.boundary.port;
let deadline = std::time::Instant::now() + PREFLIGHT_VERDICT_WAIT;
let url = format!("http://127.0.0.1:{port}/admin/boundary/status");
loop {
let status = crate::egress::blocking_client_builder()
.timeout(std::time::Duration::from_secs(2))
.build()
.ok()
.and_then(|c| c.get(&url).send().ok())
.and_then(|r| r.json::<serde_json::Value>().ok());
if let Some(body) = status {
match preflight_wait_rule(&body, &planes, cfg) {
WaitRule::Ok => {
output.print_step(&format!(
"Model boundary verified — agents routed via http://127.0.0.1:{port}"
));
return Ok(());
}
WaitRule::Err {
agent,
upstream,
reason,
} => {
let err = OlError::new(
crate::error::ERR_BOUNDARY_PREFLIGHT_FAILED,
format!("Model boundary check failed for {agent}: {reason}"),
)
.with_suggestion(format!(
"{agent}'s request plane is absent: its settings were left untouched, so \
its sessions connect straight to the provider and keep working — but \
nothing is captured. Check network reachability to {upstream} (proxy, \
VPN, TLS interception), then run `openlatch restart`. Run `openlatch \
doctor` for the full picture."
))
.with_docs("https://docs.openlatch.ai/errors/OL-BND-PREFLIGHT");
output.print_error(&err);
return Err(err);
}
WaitRule::Wait => {}
}
}
if std::time::Instant::now() >= deadline {
output.print_substep(
"Model boundary: no verdict yet — the daemon is still checking it. \
Run `openlatch doctor` in a moment to confirm.",
);
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(250));
}
}
#[cfg(feature = "boundary")]
#[derive(Debug, PartialEq, Eq)]
enum WaitRule {
Ok,
Err {
agent: &'static str,
upstream: String,
reason: String,
},
Wait,
}
#[cfg(feature = "boundary")]
fn preflight_wait_rule(
body: &serde_json::Value,
planes: &[(&'static str, crate::boundary::wire_format::WireFormat)],
cfg: &config::Config,
) -> WaitRule {
let verdicts = body.get("preflight");
let mut all_ok = true;
for (agent, fmt) in planes {
match verdicts.and_then(|v| v.get(agent)).and_then(|v| v.as_str()) {
Some("ok") => {}
Some("failed") => {
return WaitRule::Err {
agent,
upstream: cfg.boundary.upstream_for(*fmt),
reason: body
.get("preflight_error")
.and_then(|v| v.get(agent))
.and_then(|v| v.as_str())
.unwrap_or("the boundary could not complete a request to the provider")
.to_string(),
};
}
_ => all_ok = false,
}
}
if all_ok {
WaitRule::Ok
} else {
WaitRule::Wait
}
}
#[cfg(feature = "boundary")]
fn warn_boundary_disabled_in_config(config_path: &std::path::Path, output: &OutputConfig) {
if output.format != OutputFormat::Human || output.quiet {
return;
}
let line = crate::cli::color::bold(
" MODEL BOUNDARY OFF — model calls do not pass through OpenLatch ",
output.color,
);
eprintln!();
eprintln!("{}", crate::cli::color::red(&"=".repeat(66), output.color));
eprintln!("{line}");
eprintln!("{}", crate::cli::color::red(&"=".repeat(66), output.color));
eprintln!(" Nothing is captured and no policy is enforced on model traffic.");
eprintln!(
" Set by : {} — [boundary] enabled = false",
config_path.display()
);
eprintln!(" Undo by : openlatch boundary enable");
eprintln!();
}
fn build_install_report(
args: &InitArgs,
output: &OutputConfig,
) -> Option<crate::cli::report::Report> {
use crate::cli::report::{Check, Section};
let mut report = crate::cli::commands::doctor::run_all_checks(output)
.ok()?
.report;
if args.no_start {
report.replace_section(
Section::Daemon,
Check::off(Section::Daemon, "Not started at your request (--no-start)")
.code(crate::error::ERR_DAEMON_START_FAILED)
.source("--no-start")
.remedy("Run `openlatch start` when you want it up."),
);
for section in [
Section::Hooks,
Section::Boundary,
Section::Cloud,
Section::Policy,
Section::Inventory,
Section::Integrity,
] {
report.replace_section(section, Check::unknown(section, Section::Daemon));
}
}
Some(report)
}
#[cfg(test)]
mod start_plan_tests {
use super::*;
#[test]
fn active_supervision_means_init_does_not_spawn() {
assert_eq!(
plan_daemon_start(false, false, true),
DaemonStartPlan::SupervisorOwned
);
}
#[test]
fn without_supervision_init_still_spawns() {
assert_eq!(
plan_daemon_start(false, false, false),
DaemonStartPlan::SpawnBackground
);
}
#[test]
fn explicit_flags_win_over_supervision() {
assert_eq!(plan_daemon_start(true, false, true), DaemonStartPlan::Skip);
assert_eq!(
plan_daemon_start(false, true, true),
DaemonStartPlan::Foreground
);
assert_eq!(plan_daemon_start(true, true, true), DaemonStartPlan::Skip);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "boundary")]
#[test]
fn verify_boundary_preflight_waits_for_every_agent() {
use crate::boundary::wire_format::WireFormat;
let mut cfg = config::Config::defaults();
cfg.boundary.upstream.insert(
WireFormat::OpenAiResponses.as_str().to_string(),
"http://127.0.0.1:9".to_string(),
);
let planes = [
("claude-code", WireFormat::AnthropicMessages),
("codex-cli", WireFormat::OpenAiResponses),
];
let pending = serde_json::json!({
"preflight": { "claude-code": "ok" },
"preflight_error": { "claude-code": null },
});
assert_eq!(
preflight_wait_rule(&pending, &planes, &cfg),
WaitRule::Wait,
"one green plane is not the host's answer while another has no verdict"
);
let still_pending = serde_json::json!({
"preflight": { "claude-code": "ok", "codex-cli": "pending" },
});
assert_eq!(
preflight_wait_rule(&still_pending, &planes, &cfg),
WaitRule::Wait
);
let failed = serde_json::json!({
"preflight": { "claude-code": "ok", "codex-cli": "failed" },
"preflight_error": { "claude-code": null, "codex-cli": "could not reach it" },
});
match preflight_wait_rule(&failed, &planes, &cfg) {
WaitRule::Err {
agent,
upstream,
reason,
} => {
assert_eq!(agent, "codex-cli");
assert_eq!(upstream, "http://127.0.0.1:9");
assert!(
!upstream.contains("anthropic"),
"the upstream named must be the failing format's, not Anthropic's"
);
assert_eq!(reason, "could not reach it");
}
other => panic!("a failed plane must fail the install, got {other:?}"),
}
let all_ok = serde_json::json!({
"preflight": { "claude-code": "ok", "codex-cli": "ok" },
});
assert_eq!(preflight_wait_rule(&all_ok, &planes, &cfg), WaitRule::Ok);
let legacy = serde_json::json!({ "preflight": "ok" });
assert_eq!(
preflight_wait_rule(&legacy, &planes, &cfg),
WaitRule::Wait,
"a per-agent reader must not accept a scalar verdict as everyone's"
);
}
}