bamboo-server 2026.7.34

HTTP server and API layer for the Bamboo agent framework
Documentation
use actix_web::{web, HttpResponse};
use bamboo_config::{
    CredentialRef, CredentialStore, COPILOT_CHAT_CONFIG_CREDENTIAL_REF,
    COPILOT_GITHUB_ACCESS_CREDENTIAL_REF,
};

use crate::{app_state::AppState, error::AppError};

use super::types::AuthStatus;

pub async fn get_copilot_auth_status(
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, AppError> {
    let chat_ref = CredentialRef::parse(COPILOT_CHAT_CONFIG_CREDENTIAL_REF.to_string())
        .map_err(|_| copilot_status_error())?;
    if let Some(cached) = CredentialStore::open(&app_state.app_data_dir)
        .resolve(&chat_ref)
        .map_err(|_| copilot_status_error())?
    {
        let Some(status) = auth_status_from_token_content(cached.expose(), current_unix_secs())
        else {
            return Err(copilot_status_error());
        };
        return Ok(HttpResponse::Ok().json(status));
    }

    Ok(HttpResponse::Ok().json(AuthStatus {
        authenticated: false,
        message: Some("No cached token found".to_string()),
    }))
}

pub async fn logout_copilot(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
    let store = CredentialStore::open(&app_state.app_data_dir);
    for reference in [
        COPILOT_GITHUB_ACCESS_CREDENTIAL_REF,
        COPILOT_CHAT_CONFIG_CREDENTIAL_REF,
    ] {
        let reference = CredentialRef::parse(reference.to_string()).map_err(|_| {
            AppError::InternalError(anyhow::anyhow!("Copilot credential cleanup failed"))
        })?;
        store.clear_system_managed(&reference).map_err(|_| {
            AppError::InternalError(anyhow::anyhow!("Copilot credential cleanup failed"))
        })?;
    }
    tracing::info!("Copilot logged out successfully");
    Ok(HttpResponse::Ok().json(serde_json::json!({
        "success": true,
        "message": "Logged out successfully"
    })))
}

fn copilot_status_error() -> AppError {
    AppError::InternalError(anyhow::anyhow!("Copilot credential status is unavailable"))
}

pub(super) fn auth_status_from_token_content(content: &str, now: u64) -> Option<AuthStatus> {
    let token_data = serde_json::from_str::<serde_json::Value>(content).ok()?;
    let expires_at = token_data
        .get("expires_at")
        .and_then(|value| value.as_u64())?;

    if expires_at.saturating_sub(60) > now {
        let remaining = expires_at.saturating_sub(now);
        Some(AuthStatus {
            authenticated: true,
            message: Some(format!("Token expires in {} minutes", remaining / 60)),
        })
    } else {
        Some(AuthStatus {
            authenticated: false,
            message: Some("Token expired".to_string()),
        })
    }
}

fn current_unix_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}