i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{info, warn};

pub mod auth;
pub mod handlers;
pub mod routes;
pub mod state;

pub use auth::AuthConfig;
pub use routes::create_router;
pub use state::AppState;

/// Start the web dashboard / API server.
///
/// Binding rules:
/// - Default: bind to `127.0.0.1` (loopback only). No token required.
/// - If `ISELF_BIND` is set to something non-loopback (e.g. `0.0.0.0`), an
///   API token MUST be configured (`ISELF_API_TOKEN` env var or
///   `~/.i-self/api_token` file). Otherwise we refuse to start, because
///   exposing /api/ai/* publicly without auth lets anyone burn the user's
///   LLM budget.
pub async fn start_dashboard(port: u16, state: AppState) -> anyhow::Result<()> {
    let bind_host = std::env::var("ISELF_BIND")
        .ok()
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| "127.0.0.1".to_string());

    let mut auth_cfg = AuthConfig::from_env_or_disk();
    let is_loopback = bind_host == "127.0.0.1" || bind_host == "::1" || bind_host == "localhost";

    // If the user is binding non-loopback without a token configured,
    // auto-generate one and persist it to ~/.i-self/api_token. This is a
    // friendlier default than hard-erroring while still keeping the API
    // gated. The token is printed once at startup so the user can copy it.
    if !is_loopback && auth_cfg.token.is_none() {
        match autogenerate_api_token() {
            Ok((token, path)) => {
                warn!(
                    "No API token configured for non-loopback bind. Auto-generated one at {} \
                     (chmod 600). Use it as Authorization: Bearer <token>, or open the dashboard \
                     once with ?token=<token> in the URL to bootstrap the browser session.",
                    path.display()
                );
                println!("\n  ISELF_API_TOKEN={}\n", token);
                auth_cfg = AuthConfig { token: Some(token) };
            }
            Err(e) => {
                anyhow::bail!(
                    "Refusing to bind to non-loopback address {} without an API token, \
                     and could not auto-generate one ({}). Set ISELF_API_TOKEN=<token> \
                     (or write the token to ~/.i-self/api_token) before starting.",
                    bind_host,
                    e
                );
            }
        }
    }

    info!("Starting i-self web dashboard on {}:{}", bind_host, port);
    if auth_cfg.token.is_some() {
        info!("Bearer-token auth: ENABLED — requests to /api/* must carry `Authorization: Bearer <token>`");
    } else if is_loopback {
        warn!("Bearer-token auth: DISABLED (loopback only). Any local process can call /api/*.");
    }

    let app = create_router(Arc::new(RwLock::new(state)), Arc::new(auth_cfg));

    let listener = tokio::net::TcpListener::bind(format!("{}:{}", bind_host, port)).await?;
    info!("Dashboard available at http://{}:{}", bind_host, port);

    axum::serve(listener, app).await?;

    Ok(())
}

/// Generate a 256-bit random hex token and write it to `~/.i-self/api_token`
/// with mode 0600 (Unix). Returns `(token, path)` on success.
fn autogenerate_api_token() -> anyhow::Result<(String, std::path::PathBuf)> {
    use rand::RngCore;

    let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("no home directory"))?;
    let dir = home.join(".i-self");
    std::fs::create_dir_all(&dir)?;
    let path = dir.join("api_token");

    let mut bytes = [0u8; 32];
    rand::thread_rng().fill_bytes(&mut bytes);
    let token = hex_encode(&bytes);

    std::fs::write(&path, &token)?;

    // Tighten perms to 0600 so other local users can't read the token.
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(&path)?.permissions();
        perms.set_mode(0o600);
        std::fs::set_permissions(&path, perms)?;
    }

    Ok((token, path))
}

fn hex_encode(b: &[u8]) -> String {
    const H: &[u8; 16] = b"0123456789abcdef";
    let mut s = String::with_capacity(b.len() * 2);
    for &byte in b {
        s.push(H[(byte >> 4) as usize] as char);
        s.push(H[(byte & 0x0f) as usize] as char);
    }
    s
}