runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::Duration;

use runx_contracts::{JsonObject, JsonValue};
use runx_parser::{SkillMcpServer, ValidatedSkill};

use crate::credentials::SecretEnv;
use crate::sandbox::SandboxPlan;
use crate::services::process_env_snapshot;

#[derive(Clone, Debug, PartialEq)]
pub struct McpToolCallRequest {
    pub server: SkillMcpServer,
    pub tool: String,
    pub arguments: JsonObject,
    pub timeout: Duration,
    pub sandbox: SandboxPlan,
    pub secret_env: SecretEnv,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpListToolsRequest {
    pub server: SkillMcpServer,
    pub timeout: Duration,
    pub sandbox: SandboxPlan,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpToolDescriptor {
    pub name: String,
    pub description: Option<String>,
    pub input_schema: Option<JsonObject>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpServerOptions {
    pub package_name: String,
    pub package_version: String,
    pub tools: Vec<McpServerTool>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpServerExecutionOptions {
    pub runner: Option<String>,
    pub receipt_dir: Option<PathBuf>,
    pub env: BTreeMap<String, String>,
}

impl Default for McpServerExecutionOptions {
    fn default() -> Self {
        Self {
            runner: None,
            receipt_dir: None,
            env: process_env_snapshot(),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpServerTool {
    pub name: String,
    pub description: String,
    pub input_schema: JsonObject,
    pub required_scopes: Vec<String>,
    pub result: McpServerToolBehavior,
}

#[derive(Clone, Debug, PartialEq)]
pub enum McpServerToolBehavior {
    Fixed(McpToolResult),
    Skill(Box<McpServerSkillExecution>),
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpServerSkillExecution {
    pub skill_path: PathBuf,
    pub skill: ValidatedSkill,
    pub receipt_dir: Option<PathBuf>,
    pub env: BTreeMap<String, String>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpToolResult {
    pub content: Vec<McpContent>,
    pub structured_content: Option<JsonObject>,
    pub is_error: bool,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpContent {
    pub text: String,
}

#[derive(Clone, Debug, PartialEq)]
pub enum McpHostRunResult {
    Completed {
        skill_name: String,
        output: String,
        receipt_id: String,
        runx: JsonObject,
    },
    NeedsAgent {
        skill_name: String,
        run_id: String,
        request_count: usize,
        runx: JsonObject,
    },
    Denied {
        skill_name: String,
        receipt_id: Option<String>,
        runx: JsonObject,
    },
    Escalated {
        skill_name: String,
        receipt_id: String,
        error: String,
        runx: JsonObject,
    },
    Failed {
        skill_name: String,
        receipt_id: Option<String>,
        error: String,
        runx: JsonObject,
    },
}

#[derive(Debug)]
pub struct McpServerError {
    message: String,
}

impl McpServerError {
    pub(super) fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl std::fmt::Display for McpServerError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for McpServerError {}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct McpTransportError {
    kind: McpTransportErrorKind,
    message: String,
}

impl McpTransportError {
    #[must_use]
    pub fn failed(message: impl Into<String>) -> Self {
        Self {
            kind: McpTransportErrorKind::Failed,
            message: message.into(),
        }
    }

    #[must_use]
    pub fn tool_error(code: i64, message: impl Into<String>) -> Self {
        Self {
            kind: McpTransportErrorKind::ToolError(code),
            message: message.into(),
        }
    }

    #[must_use]
    pub fn timeout(timeout: Duration) -> Self {
        let timeout_ms = u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX);
        Self {
            kind: McpTransportErrorKind::Timeout,
            message: format!("MCP call timed out after {timeout_ms}ms."),
        }
    }

    #[must_use]
    pub fn sanitized_message(&self) -> String {
        match self.kind {
            McpTransportErrorKind::ToolError(code) => {
                format!("MCP tool returned error {code}.")
            }
            McpTransportErrorKind::Timeout => self.message.clone(),
            McpTransportErrorKind::Failed => "MCP adapter failed.".to_owned(),
        }
    }

    #[cfg(all(test, feature = "mcp"))]
    #[must_use]
    pub(super) fn message_for_test(&self) -> &str {
        &self.message
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum McpTransportErrorKind {
    ToolError(i64),
    Timeout,
    Failed,
}

pub trait McpTransport {
    fn call_tool(&self, request: McpToolCallRequest) -> Result<JsonValue, McpTransportError>;
}

impl<T> McpTransport for &T
where
    T: McpTransport + ?Sized,
{
    fn call_tool(&self, request: McpToolCallRequest) -> Result<JsonValue, McpTransportError> {
        (**self).call_tool(request)
    }
}