bamboo-server 2026.4.25

HTTP server and API layer for the Bamboo agent framework
Documentation
use actix_web::http::{header, StatusCode};
use actix_web::{test, web, App};
use tempfile::tempdir;

use super::{configure_routes, configure_routes_with_rate_limiting};
use crate::AppState;
use bamboo_infrastructure::AccessControlConfig;

#[actix_web::test]
async fn configure_routes_registers_expected_api_prefixes() {
    let app = test::init_service(App::new().configure(configure_routes)).await;

    let requests = vec![
        ("GET", "/api/v1/health"),
        ("POST", "/api/v1/sessions/example/project-dream/run"),
        ("GET", "/v1/bamboo/workflows"),
        ("GET", "/v1/bamboo/access/status"),
        ("GET", "/openai/v1/models"),
        ("GET", "/anthropic/v1/models"),
        ("GET", "/gemini/v1beta/models"),
    ];

    for (method, uri) in requests {
        let req = match method {
            "POST" => test::TestRequest::post().uri(uri).to_request(),
            _ => test::TestRequest::get().uri(uri).to_request(),
        };
        let resp = test::call_service(&app, req).await;
        assert_ne!(
            resp.status(),
            StatusCode::NOT_FOUND,
            "expected route to be registered: {method} {uri}"
        );
    }
}

#[actix_web::test]
async fn configure_routes_with_rate_limiting_registers_expected_api_prefixes() {
    let app = test::init_service(App::new().configure(configure_routes_with_rate_limiting)).await;

    let requests = vec![
        ("GET", "/api/v1/health"),
        ("POST", "/api/v1/sessions/example/project-dream/run"),
        ("GET", "/v1/bamboo/workflows"),
        ("GET", "/v1/bamboo/access/status"),
        ("GET", "/openai/v1/models"),
        ("GET", "/anthropic/v1/models"),
        ("GET", "/gemini/v1beta/models"),
    ];

    for (method, uri) in requests {
        let req = match method {
            "POST" => test::TestRequest::post().uri(uri).to_request(),
            _ => test::TestRequest::get().uri(uri).to_request(),
        };
        let resp = test::call_service(&app, req).await;
        assert_ne!(
            resp.status(),
            StatusCode::NOT_FOUND,
            "expected route to be registered: {method} {uri}"
        );
    }
}

#[actix_web::test]
async fn remote_unverified_request_is_blocked_by_access_middleware() {
    let data_dir = tempdir().unwrap();
    let app_state = web::Data::new(AppState::new(data_dir.path().to_path_buf()).await.unwrap());
    {
        let mut config = app_state.config.write().await;
        config.access_control = Some(AccessControlConfig {
            password_enabled: true,
            password_hash: Some(
                "a65192f8d645bc4d19765b8ea61bfbb896dc999cb88a4be419518c5493f92c9d".to_string(),
            ),
            password_salt: Some("01010101010101010101010101010101".to_string()),
            updated_at: None,
        });
    }
    let app = test::init_service(App::new().app_data(app_state).configure(configure_routes)).await;

    let req = test::TestRequest::get()
        .uri("/v1/bamboo/workflows")
        .insert_header((header::HOST, "bamboo.example.com"))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[actix_web::test]
async fn access_bootstrap_endpoints_remain_public() {
    let data_dir = tempdir().unwrap();
    let app_state = web::Data::new(AppState::new(data_dir.path().to_path_buf()).await.unwrap());
    {
        let mut config = app_state.config.write().await;
        config.access_control = Some(AccessControlConfig {
            password_enabled: true,
            password_hash: Some(
                "a65192f8d645bc4d19765b8ea61bfbb896dc999cb88a4be419518c5493f92c9d".to_string(),
            ),
            password_salt: Some("01010101010101010101010101010101".to_string()),
            updated_at: None,
        });
    }
    let app = test::init_service(App::new().app_data(app_state).configure(configure_routes)).await;

    for req in [
        test::TestRequest::get()
            .uri("/v1/bamboo/access/status")
            .insert_header((header::HOST, "bamboo.example.com"))
            .to_request(),
        test::TestRequest::get()
            .uri("/api/v1/health")
            .insert_header((header::HOST, "bamboo.example.com"))
            .to_request(),
    ] {
        let resp = test::call_service(&app, req).await;
        assert_ne!(resp.status(), StatusCode::UNAUTHORIZED);
    }
}

#[actix_web::test]
async fn verified_cookie_allows_remote_request_through_middleware() {
    let data_dir = tempdir().unwrap();
    let app_state = web::Data::new(AppState::new(data_dir.path().to_path_buf()).await.unwrap());
    {
        let mut config = app_state.config.write().await;
        config.access_control = Some(AccessControlConfig {
            password_enabled: true,
            password_hash: Some(
                "a65192f8d645bc4d19765b8ea61bfbb896dc999cb88a4be419518c5493f92c9d".to_string(),
            ),
            password_salt: Some("01010101010101010101010101010101".to_string()),
            updated_at: None,
        });
    }
    let app = test::init_service(App::new().app_data(app_state).configure(configure_routes)).await;

    let verify_req = test::TestRequest::post()
        .uri("/v1/bamboo/access/verify")
        .insert_header((header::HOST, "bamboo.example.com"))
        .set_json(serde_json::json!({ "password": "secret" }))
        .to_request();
    let verify_resp = test::call_service(&app, verify_req).await;
    assert_eq!(verify_resp.status(), StatusCode::OK);

    let set_cookie = verify_resp
        .headers()
        .get(header::SET_COOKIE)
        .expect("verify response should set cookie")
        .to_str()
        .unwrap()
        .split(';')
        .next()
        .unwrap()
        .to_string();

    let protected_req = test::TestRequest::get()
        .uri("/v1/bamboo/workflows")
        .insert_header((header::HOST, "bamboo.example.com"))
        .insert_header((header::COOKIE, set_cookie))
        .to_request();
    let protected_resp = test::call_service(&app, protected_req).await;
    assert_eq!(protected_resp.status(), StatusCode::OK);
}

#[actix_web::test]
async fn system_prompt_snapshot_route_returns_project_dream_over_http() {
    let data_dir = tempdir().unwrap();
    bamboo_infrastructure::paths::init_bamboo_dir(data_dir.path().to_path_buf());
    let app_state = web::Data::new(AppState::new(data_dir.path().to_path_buf()).await.unwrap());

    let mut session = bamboo_agent_core::Session::new("session-http-project-dream", "gpt-5");
    session.add_message(bamboo_agent_core::Message::system(
        "Base prompt\n\n<!-- BAMBOO_EXTERNAL_MEMORY_START -->\n## External Memory (Persistent)\n\n### Project Dream Summary\n````md\nHTTP project dream content\n````\n\n### Session Memory Note (markdown)\n````md\nHTTP session note content\n````\n<!-- BAMBOO_EXTERNAL_MEMORY_END -->",
    ));
    app_state
        .storage
        .save_session(&session)
        .await
        .expect("save session");

    let app = test::init_service(App::new().app_data(app_state).configure(configure_routes)).await;

    let req = test::TestRequest::get()
        .uri("/api/v1/sessions/session-http-project-dream/system-prompt")
        .insert_header((header::HOST, "localhost:9562"))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);

    let body = actix_web::body::to_bytes(resp.into_body())
        .await
        .expect("read response body");
    let payload: serde_json::Value = serde_json::from_slice(&body).expect("parse response payload");

    assert_eq!(
        payload["dream_notebook"],
        serde_json::json!("HTTP project dream content")
    );
    assert_eq!(
        payload["project_dream"],
        serde_json::json!("HTTP project dream content")
    );
    assert_eq!(
        payload["session_memory_note"],
        serde_json::json!("HTTP session note content")
    );
    assert!(payload.get("global_dream_fallback").is_none());
    assert!(payload["external_memory"]
        .as_str()
        .unwrap_or_default()
        .contains("### Project Dream Summary"));
}

#[actix_web::test]
async fn local_request_bypasses_access_middleware() {
    let data_dir = tempdir().unwrap();
    let app_state = web::Data::new(AppState::new(data_dir.path().to_path_buf()).await.unwrap());
    {
        let mut config = app_state.config.write().await;
        config.access_control = Some(AccessControlConfig {
            password_enabled: true,
            password_hash: Some(
                "a65192f8d645bc4d19765b8ea61bfbb896dc999cb88a4be419518c5493f92c9d".to_string(),
            ),
            password_salt: Some("01010101010101010101010101010101".to_string()),
            updated_at: None,
        });
    }
    let app = test::init_service(App::new().app_data(app_state).configure(configure_routes)).await;

    let req = test::TestRequest::get()
        .uri("/v1/bamboo/workflows")
        .insert_header((header::HOST, "localhost:9562"))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);
}