tandem-server 0.4.16

HTTP server for Tandem engine APIs
Documentation
use super::*;
use std::path::PathBuf;

#[derive(Debug, Deserialize)]
pub(super) struct PackSelectorPath {
    pub selector: 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_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
    })))
}

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",
    })))
}