tandem-server 0.4.25

HTTP server for Tandem engine APIs
Documentation
use super::*;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::Response;
use serde_json::{json, Value};
use std::path::PathBuf;

use crate::http::global::sanitize_relative_subpath;

#[derive(Debug, Deserialize)]
pub(super) struct PackSelectorPath {
    pub selector: String,
}

#[derive(Debug, Deserialize)]
pub(super) struct PackSelectorFilePath {
    pub selector: String,
    pub path: String,
}

#[derive(Debug, Deserialize)]
pub(super) struct PackDetectInput {
    pub path: String,
    #[serde(default)]
    pub attachment_id: Option<String>,
    #[serde(default)]
    pub connector: Option<String>,
    #[serde(default)]
    pub channel_id: Option<String>,
    #[serde(default)]
    pub sender_id: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(super) struct PackInstallFromAttachmentInput {
    pub attachment_id: String,
    pub path: String,
    #[serde(default)]
    pub connector: Option<String>,
    #[serde(default)]
    pub channel_id: Option<String>,
    #[serde(default)]
    pub sender_id: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
pub(super) struct PackUpdateApplyInput {
    #[serde(default)]
    pub target_version: Option<String>,
}

pub(super) async fn packs_list(State(state): State<AppState>) -> Result<Json<Value>, StatusCode> {
    let packs = state.pack_manager.list().await.map_err(|err| {
        tracing::warn!("packs list failed: {}", err);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    Ok(Json(json!({ "packs": packs })))
}

pub(super) async fn packs_get(
    State(state): State<AppState>,
    Path(PackSelectorPath { selector }): Path<PackSelectorPath>,
) -> Result<Json<Value>, StatusCode> {
    let inspection = state.pack_manager.inspect(&selector).await.map_err(|err| {
        if err.to_string().contains("not found") {
            StatusCode::NOT_FOUND
        } else {
            tracing::warn!("pack inspect failed: {}", err);
            StatusCode::INTERNAL_SERVER_ERROR
        }
    })?;
    Ok(Json(json!({
        "pack": inspection,
    })))
}

pub(super) async fn packs_file_get(
    State(state): State<AppState>,
    Path(PackSelectorFilePath { selector, path }): Path<PackSelectorFilePath>,
) -> Result<Response, StatusCode> {
    let rel = sanitize_relative_subpath(Some(&path))?;
    let inspection = state.pack_manager.inspect(&selector).await.map_err(|err| {
        if err.to_string().contains("not found") {
            StatusCode::NOT_FOUND
        } else {
            tracing::warn!("pack file inspect failed: {}", err);
            StatusCode::INTERNAL_SERVER_ERROR
        }
    })?;
    let file_path = PathBuf::from(&inspection.installed.install_path).join(&rel);
    if !file_path.exists() || !file_path.is_file() {
        return Err(StatusCode::NOT_FOUND);
    }
    let bytes = tokio::fs::read(&file_path).await.map_err(|err| {
        tracing::warn!("pack file read failed: {}", err);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    let mut response = Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, content_type_for_path(&file_path))
        .header(header::CACHE_CONTROL, "no-store")
        .body(Body::from(bytes))
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    response.headers_mut().insert(
        header::X_CONTENT_TYPE_OPTIONS,
        HeaderValue::from_static("nosniff"),
    );
    Ok(response)
}

pub(super) async fn packs_install(
    State(state): State<AppState>,
    Json(input): Json<PackInstallRequest>,
) -> Result<Json<Value>, StatusCode> {
    state.event_bus.publish(EngineEvent::new(
        "pack.install.started",
        json!({
            "source": input.source,
            "path": input.path,
            "url": input.url,
        }),
    ));
    let result = state.pack_manager.install(input).await;
    match result {
        Ok(installed) => {
            state.event_bus.publish(EngineEvent::new(
                "pack.install.succeeded",
                json!({
                    "pack_id": installed.pack_id,
                    "name": installed.name,
                    "version": installed.version,
                }),
            ));
            state.event_bus.publish(EngineEvent::new(
                "registry.updated",
                json!({ "entity": "packs" }),
            ));
            Ok(Json(json!({ "installed": installed })))
        }
        Err(err) => {
            state.event_bus.publish(EngineEvent::new(
                "pack.install.failed",
                json!({
                    "error": err.to_string(),
                    "code": "pack_install_failed",
                }),
            ));
            Err(StatusCode::BAD_REQUEST)
        }
    }
}

pub(super) async fn packs_install_from_attachment(
    State(state): State<AppState>,
    Json(input): Json<PackInstallFromAttachmentInput>,
) -> Result<Json<Value>, StatusCode> {
    let source = json!({
        "kind": "attachment",
        "attachment_id": input.attachment_id,
        "connector": input.connector,
        "channel_id": input.channel_id,
        "sender_id": input.sender_id,
    });
    packs_install(
        State(state),
        Json(PackInstallRequest {
            path: Some(input.path),
            url: None,
            source,
        }),
    )
    .await
}

pub(super) async fn packs_uninstall(
    State(state): State<AppState>,
    Json(input): Json<PackUninstallRequest>,
) -> Result<Json<Value>, StatusCode> {
    let removed = state.pack_manager.uninstall(input).await.map_err(|err| {
        if err.to_string().contains("not found") {
            StatusCode::NOT_FOUND
        } else {
            tracing::warn!("pack uninstall failed: {}", err);
            StatusCode::INTERNAL_SERVER_ERROR
        }
    })?;
    state.event_bus.publish(EngineEvent::new(
        "registry.updated",
        json!({ "entity": "packs" }),
    ));
    Ok(Json(json!({ "removed": removed })))
}

pub(super) async fn packs_export(
    State(state): State<AppState>,
    Json(input): Json<PackExportRequest>,
) -> Result<Json<Value>, StatusCode> {
    let exported = state.pack_manager.export(input).await.map_err(|err| {
        tracing::warn!("pack export failed: {}", err);
        StatusCode::BAD_REQUEST
    })?;
    Ok(Json(json!({ "exported": exported })))
}

pub(super) async fn packs_updates_get(
    State(state): State<AppState>,
    Path(PackSelectorPath { selector }): Path<PackSelectorPath>,
) -> Result<Json<Value>, StatusCode> {
    let inspection = state.pack_manager.inspect(&selector).await.map_err(|err| {
        if err.to_string().contains("not found") {
            StatusCode::NOT_FOUND
        } else {
            tracing::warn!("pack updates check failed: {}", err);
            StatusCode::INTERNAL_SERVER_ERROR
        }
    })?;
    Ok(Json(json!({
        "pack_id": inspection.installed.pack_id,
        "name": inspection.installed.name,
        "current_version": inspection.installed.version,
        "updates": [],
        "permissions_diff": {
            "added_required_capabilities": [],
            "removed_required_capabilities": [],
            "added_provider_specific_dependencies": [],
            "removed_provider_specific_dependencies": [],
            "routine_scope_changed": false
        },
        "reapproval_required": false
    })))
}

fn content_type_for_path(path: &std::path::Path) -> &'static str {
    match path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase()
        .as_str()
    {
        "svg" => "image/svg+xml",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "md" | "markdown" | "mdx" => "text/markdown; charset=utf-8",
        "json" => "application/json; charset=utf-8",
        "yaml" | "yml" => "text/yaml; charset=utf-8",
        "txt" | "log" => "text/plain; charset=utf-8",
        _ => "application/octet-stream",
    }
}

pub(super) async fn packs_update_post(
    State(state): State<AppState>,
    Path(PackSelectorPath { selector }): Path<PackSelectorPath>,
    Json(input): Json<PackUpdateApplyInput>,
) -> Result<Json<Value>, StatusCode> {
    let inspection = state.pack_manager.inspect(&selector).await.map_err(|err| {
        if err.to_string().contains("not found") {
            StatusCode::NOT_FOUND
        } else {
            tracing::warn!("pack update apply failed: {}", err);
            StatusCode::INTERNAL_SERVER_ERROR
        }
    })?;
    state.event_bus.publish(EngineEvent::new(
        "pack.update.not_available",
        json!({
            "pack_id": inspection.installed.pack_id,
            "name": inspection.installed.name,
            "current_version": inspection.installed.version,
            "target_version": input.target_version,
            "reason": "updates_not_implemented",
        }),
    ));
    Ok(Json(json!({
        "updated": false,
        "pack_id": inspection.installed.pack_id,
        "name": inspection.installed.name,
        "current_version": inspection.installed.version,
        "target_version": input.target_version,
        "reason": "updates_not_implemented",
        "permissions_diff": {
            "added_required_capabilities": [],
            "removed_required_capabilities": [],
            "added_provider_specific_dependencies": [],
            "removed_provider_specific_dependencies": [],
            "routine_scope_changed": false
        },
        "reapproval_required": false
    })))
}

pub(super) async fn packs_detect(
    State(state): State<AppState>,
    Json(input): Json<PackDetectInput>,
) -> Result<Json<Value>, StatusCode> {
    let path = PathBuf::from(&input.path);
    let is_pack = state.pack_manager.detect(&path).await.map_err(|err| {
        tracing::warn!("pack detect failed: {}", err);
        StatusCode::BAD_REQUEST
    })?;
    if is_pack {
        state.event_bus.publish(EngineEvent::new(
            "pack.detected",
            json!({
                "path": input.path,
                "attachment_id": input.attachment_id,
                "connector": input.connector,
                "channel_id": input.channel_id,
                "sender_id": input.sender_id,
                "marker": "tandempack.yaml",
            }),
        ));
    }
    Ok(Json(json!({
        "is_pack": is_pack,
        "marker": "tandempack.yaml",
    })))
}