kcode-dev-tools 0.2.0

Session-scoped tools for managed Rust libraries, Web libraries, and Rust binaries
Documentation
//! Session-scoped tools for three managed-source backends.

mod backend;
mod registry;

use std::error::Error as StdError;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use http::StatusCode;
use serde_json::Value;
use zeroize::Zeroizing;

pub use kcode_rust_bins::{ObjectStore, ObjectStoreError, ObjectStoreResult};

use registry::RegistryState;

pub const CREATE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/create";
pub const OPEN_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/open";
pub const DOCS_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/docs";
pub const WRITE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/write";
pub const WRITE_FILE_FREEFORM_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/write-file-freeform";
pub const DELETE_FILE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/delete-file";
pub const CHECK_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/check";
pub const PUBLISH_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/publish";
pub const PREVIEW_WRITE_FILE_RUST_LIB_TOOL: &str = "kcode-rust-libs-v2/internal-preview-write-file";

pub const CREATE_WEB_LIB_TOOL: &str = "kcode-web-libs/create";
pub const OPEN_WEB_LIB_TOOL: &str = "kcode-web-libs/open";
pub const DOCS_WEB_LIB_TOOL: &str = "kcode-web-libs/docs";
pub const WRITE_WEB_LIB_TOOL: &str = "kcode-web-libs/write";
pub const WRITE_FILE_FREEFORM_WEB_LIB_TOOL: &str = "kcode-web-libs/write-file-freeform";
pub const DELETE_FILE_WEB_LIB_TOOL: &str = "kcode-web-libs/delete-file";
pub const CHECK_WEB_LIB_TOOL: &str = "kcode-web-libs/check";
pub const PUBLISH_WEB_LIB_TOOL: &str = "kcode-web-libs/publish";
pub const PREVIEW_WRITE_FILE_WEB_LIB_TOOL: &str = "kcode-web-libs/internal-preview-write-file";

pub const CREATE_RUST_BIN_TOOL: &str = "kcode-rust-bins/create";
pub const OPEN_RUST_BIN_TOOL: &str = "kcode-rust-bins/open";
pub const DOCS_RUST_BIN_TOOL: &str = "kcode-rust-bins/docs";
pub const WRITE_RUST_BIN_TOOL: &str = "kcode-rust-bins/write";
pub const WRITE_FILE_FREEFORM_RUST_BIN_TOOL: &str = "kcode-rust-bins/write-file-freeform";
pub const DELETE_FILE_RUST_BIN_TOOL: &str = "kcode-rust-bins/delete-file";
pub const CHECK_RUST_BIN_TOOL: &str = "kcode-rust-bins/check";
pub const PUBLISH_RUST_BIN_TOOL: &str = "kcode-rust-bins/publish";
pub const CALL_RUST_BIN_TOOL: &str = "kcode-rust-bins/call";
pub const PREVIEW_WRITE_FILE_RUST_BIN_TOOL: &str = "kcode-rust-bins/internal-preview-write-file";

pub const RUST_LIB_TOOLS: [&str; 8] = [
    CREATE_RUST_LIB_TOOL,
    OPEN_RUST_LIB_TOOL,
    DOCS_RUST_LIB_TOOL,
    WRITE_RUST_LIB_TOOL,
    WRITE_FILE_FREEFORM_RUST_LIB_TOOL,
    DELETE_FILE_RUST_LIB_TOOL,
    CHECK_RUST_LIB_TOOL,
    PUBLISH_RUST_LIB_TOOL,
];

pub const WEB_LIB_TOOLS: [&str; 8] = [
    CREATE_WEB_LIB_TOOL,
    OPEN_WEB_LIB_TOOL,
    DOCS_WEB_LIB_TOOL,
    WRITE_WEB_LIB_TOOL,
    WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
    DELETE_FILE_WEB_LIB_TOOL,
    CHECK_WEB_LIB_TOOL,
    PUBLISH_WEB_LIB_TOOL,
];

pub const RUST_BIN_TOOLS: [&str; 9] = [
    CREATE_RUST_BIN_TOOL,
    OPEN_RUST_BIN_TOOL,
    DOCS_RUST_BIN_TOOL,
    WRITE_RUST_BIN_TOOL,
    WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
    DELETE_FILE_RUST_BIN_TOOL,
    CHECK_RUST_BIN_TOOL,
    PUBLISH_RUST_BIN_TOOL,
    CALL_RUST_BIN_TOOL,
];

const SESSION_LEASE: Duration = Duration::from_secs(24 * 60 * 60);
const MAX_SESSION_ID_BYTES: usize = 128;

pub struct Config {
    pub rust_libraries_root: PathBuf,
    pub web_libraries_root: PathBuf,
    pub web_publications_root: PathBuf,
    pub rust_binaries_root: PathBuf,
    pub rust_binary_publications_root: PathBuf,
    pub crates_io_registry_token: String,
    pub object_store: Arc<dyn ObjectStore>,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ManagedSourceKind {
    RustLibrary,
    WebLibrary,
    RustBinary,
}

impl ManagedSourceKind {
    pub fn label(self) -> &'static str {
        match self {
            Self::RustLibrary => "Rust library",
            Self::WebLibrary => "Web library",
            Self::RustBinary => "Rust binary",
        }
    }

    pub fn open_tool(self) -> &'static str {
        match self {
            Self::RustLibrary => OPEN_RUST_LIB_TOOL,
            Self::WebLibrary => OPEN_WEB_LIB_TOOL,
            Self::RustBinary => OPEN_RUST_BIN_TOOL,
        }
    }

    pub(crate) fn code(self, suffix: &'static str) -> &'static str {
        match (self, suffix) {
            (Self::RustLibrary, "already_open") => "rust_lib_already_open",
            (Self::RustLibrary, "not_open") => "rust_lib_not_open",
            (Self::RustLibrary, "session_ending") => "rust_lib_session_ending",
            (Self::WebLibrary, "already_open") => "web_lib_already_open",
            (Self::WebLibrary, "not_open") => "web_lib_not_open",
            (Self::WebLibrary, "session_ending") => "web_lib_session_ending",
            (Self::RustBinary, "already_open") => "rust_bin_already_open",
            (Self::RustBinary, "not_open") => "rust_bin_not_open",
            (Self::RustBinary, "session_ending") => "rust_bin_session_ending",
            _ => "managed_source_error",
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceSnapshot {
    pub kind: ManagedSourceKind,
    pub name: String,
    pub text: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolExecution {
    pub text: String,
    pub snapshot: Option<SourceSnapshot>,
}

#[derive(Debug)]
pub struct ToolError {
    pub status: StatusCode,
    pub code: &'static str,
    pub message: String,
}

#[derive(Clone)]
pub struct Service {
    pub(crate) inner: Arc<ServiceInner>,
}

pub(crate) struct ServiceInner {
    pub(crate) rust_libraries_root: PathBuf,
    pub(crate) web_libraries_root: PathBuf,
    pub(crate) web_publications_root: PathBuf,
    pub(crate) rust_binaries_root: PathBuf,
    pub(crate) rust_binary_publications_root: PathBuf,
    pub(crate) registry_token: Zeroizing<String>,
    pub(crate) object_store: Arc<dyn ObjectStore>,
    pub(crate) registry: Arc<RegistryState>,
}

impl Service {
    pub fn open(config: Config) -> Result<Self, ToolError> {
        let registry_token = config.crates_io_registry_token.trim();
        if registry_token.is_empty() {
            return Err(ToolError::unavailable(
                "registry_token_unavailable",
                "The crates.io registry token is empty.",
            ));
        }

        let web_libraries_root = checked_root(&config.web_libraries_root, "managed Web libraries")?;
        let web_publications_root =
            checked_root(&config.web_publications_root, "Web library publications")?;
        if web_libraries_root.starts_with(&web_publications_root)
            || web_publications_root.starts_with(&web_libraries_root)
        {
            return Err(ToolError::unprocessable(
                "dev_tools_invalid_root",
                "Managed Web source and publication roots must be disjoint.",
            ));
        }

        Ok(Self {
            inner: Arc::new(ServiceInner {
                rust_libraries_root: checked_root(
                    &config.rust_libraries_root,
                    "managed Rust libraries",
                )?,
                web_libraries_root,
                web_publications_root,
                rust_binaries_root: checked_root(
                    &config.rust_binaries_root,
                    "managed Rust binaries",
                )?,
                rust_binary_publications_root: checked_root(
                    &config.rust_binary_publications_root,
                    "Rust binary publications",
                )?,
                registry_token: Zeroizing::new(registry_token.to_owned()),
                object_store: config.object_store,
                registry: Arc::new(RegistryState::default()),
            }),
        })
    }

    pub fn web_libraries_root(&self) -> &Path {
        &self.inner.web_libraries_root
    }

    pub fn web_publications_root(&self) -> &Path {
        &self.inner.web_publications_root
    }

    pub async fn execute(
        &self,
        session_id: impl Into<String>,
        tool_name: impl Into<String>,
        arguments: Value,
    ) -> Result<ToolExecution, ToolError> {
        let session_id = session_id.into();
        validate_session_id(&session_id)?;
        let tool_name = tool_name.into();
        let service = self.clone();
        tokio::task::spawn_blocking(move || {
            service.execute_blocking(&session_id, &tool_name, arguments)
        })
        .await
        .map_err(|error| {
            tracing::error!(error=%error, "managed-source tool worker failed");
            ToolError::internal("The managed-source tool worker stopped unexpectedly.")
        })?
    }

    pub async fn release(&self, session_id: impl Into<String>) -> Result<usize, ToolError> {
        let session_id = session_id.into();
        validate_session_id(&session_id)?;
        let service = self.clone();
        tokio::task::spawn_blocking(move || service.release_blocking(&session_id))
            .await
            .map_err(|error| {
                tracing::error!(error=%error, "managed-source release worker failed");
                ToolError::internal("The managed-source release worker stopped unexpectedly.")
            })?
    }
}

impl ToolExecution {
    pub(crate) fn plain(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            snapshot: None,
        }
    }

    pub(crate) fn snapshot(snapshot: SourceSnapshot) -> Self {
        Self {
            text: snapshot.text.clone(),
            snapshot: Some(snapshot),
        }
    }

    pub(crate) fn with_snapshot(text: impl Into<String>, snapshot: SourceSnapshot) -> Self {
        Self {
            text: text.into(),
            snapshot: Some(snapshot),
        }
    }
}

impl ToolError {
    pub(crate) fn invalid(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: "invalid_arguments",
            message: message.into(),
        }
    }

    pub(crate) fn not_found(code: &'static str, message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            code,
            message: message.into(),
        }
    }

    pub(crate) fn conflict(code: &'static str, message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::CONFLICT,
            code,
            message: message.into(),
        }
    }

    pub(crate) fn unprocessable(code: &'static str, message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::UNPROCESSABLE_ENTITY,
            code,
            message: message.into(),
        }
    }

    pub(crate) fn unavailable(code: &'static str, message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::SERVICE_UNAVAILABLE,
            code,
            message: message.into(),
        }
    }

    pub(crate) fn internal(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: "dev_tools_internal_error",
            message: message.into(),
        }
    }
}

impl fmt::Display for ToolError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}: {}", self.code, self.message)
    }
}

impl StdError for ToolError {}

pub fn proposed_write_snapshot(tool_name: &str, arguments: &Value) -> Option<SourceSnapshot> {
    backend::proposed_write_snapshot(tool_name, arguments)
}

fn checked_root(path: &Path, label: &str) -> Result<PathBuf, ToolError> {
    std::fs::create_dir_all(path).map_err(|error| {
        ToolError::unavailable(
            "dev_tools_storage_unavailable",
            format!("Could not create {label} root {}: {error}", path.display()),
        )
    })?;
    let metadata = std::fs::symlink_metadata(path).map_err(|error| {
        ToolError::unavailable(
            "dev_tools_storage_unavailable",
            format!("Could not inspect {label} root {}: {error}", path.display()),
        )
    })?;
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        return Err(ToolError::unprocessable(
            "dev_tools_invalid_root",
            format!("{label} root {} must be a real directory.", path.display()),
        ));
    }
    std::fs::canonicalize(path).map_err(|error| {
        ToolError::unavailable(
            "dev_tools_storage_unavailable",
            format!(
                "Could not canonicalize {label} root {}: {error}",
                path.display()
            ),
        )
    })
}

fn validate_session_id(session_id: &str) -> Result<(), ToolError> {
    if session_id.is_empty()
        || session_id.len() > MAX_SESSION_ID_BYTES
        || !session_id
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b':'))
    {
        return Err(ToolError::invalid("The hidden tool session ID is invalid."));
    }
    Ok(())
}