runx-runtime 0.1.0

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::fmt;

use runx_contracts::{JsonObject, JsonValue, json_string_field};
use runx_parser::{
    ParseError, SkillInstallError, SkillInstallOrigin, ValidateSkillMode, ValidateSkillOptions,
    ValidationError, extract_skill_quality_profile, parse_graph_yaml, parse_runner_manifest_yaml,
    parse_skill_markdown, parse_tool_manifest_json, runner::resolve_post_run_reflect_policy,
    validate_graph, validate_runner_manifest, validate_skill_artifact_contract,
    validate_skill_install, validate_skill_source, validate_skill_with_options,
    validate_tool_manifest,
};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ParserEvalOutput {
    Output { value: JsonValue },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParserEvalError {
    InvalidDocument(String),
    InvalidInput(String),
    Parse(String),
    Validation(String),
    SerializeOutput(String),
}

impl ParserEvalError {
    #[must_use]
    pub fn code(&self) -> &'static str {
        match self {
            Self::InvalidDocument(_) => "invalid_document",
            Self::InvalidInput(_) => "invalid_input",
            Self::Parse(_) => "parse_error",
            Self::Validation(_) => "validation_error",
            Self::SerializeOutput(_) => "serialize_output",
        }
    }
}

impl fmt::Display for ParserEvalError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidDocument(message)
            | Self::InvalidInput(message)
            | Self::Parse(message)
            | Self::Validation(message)
            | Self::SerializeOutput(message) => formatter.write_str(message),
        }
    }
}

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

impl From<ParseError> for ParserEvalError {
    fn from(error: ParseError) -> Self {
        Self::Parse(error.to_string())
    }
}

impl From<ValidationError> for ParserEvalError {
    fn from(error: ValidationError) -> Self {
        Self::Validation(error.to_string())
    }
}

impl From<SkillInstallError> for ParserEvalError {
    fn from(error: SkillInstallError) -> Self {
        Self::Validation(error.to_string())
    }
}

pub fn evaluate_parser_document_str(source: &str) -> Result<ParserEvalOutput, ParserEvalError> {
    let document = serde_json::from_str::<JsonValue>(source)
        .map_err(|error| ParserEvalError::InvalidDocument(error.to_string()))?;
    if let Some(kind) = parser_document_kind(&document)
        && !is_supported_parser_kind(kind)
    {
        return Err(ParserEvalError::InvalidInput(format!(
            "unsupported parser input kind '{kind}'"
        )));
    }
    let input = serde_json::from_str::<ParserDocument>(source)
        .map_err(|error| ParserEvalError::InvalidInput(error.to_string()))?;
    Ok(ParserEvalOutput::Output {
        value: evaluate_parser_input(input)?,
    })
}

fn parser_document_kind(document: &JsonValue) -> Option<&str> {
    let JsonValue::Object(fields) = document else {
        return None;
    };
    match fields.get("input") {
        Some(JsonValue::Object(input)) => json_string_field(input, "kind"),
        _ => json_string_field(fields, "kind"),
    }
}

fn is_supported_parser_kind(kind: &str) -> bool {
    matches!(
        kind,
        "parser.validateSkillMarkdown"
            | "parser.validateRunnerManifestYaml"
            | "parser.validateGraphYaml"
            | "parser.validateToolManifestJson"
            | "parser.validateSkillSource"
            | "parser.validateSkillArtifactContract"
            | "parser.extractSkillQualityProfile"
            | "parser.resolvePostRunReflectPolicy"
            | "parser.validateSkillInstall"
    )
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ParserDocument {
    Envelope { input: ParserInput },
    Input(ParserInput),
}

impl From<ParserDocument> for ParserInput {
    fn from(document: ParserDocument) -> Self {
        match document {
            ParserDocument::Envelope { input } | ParserDocument::Input(input) => input,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(tag = "kind", rename_all_fields = "camelCase")]
enum ParserInput {
    #[serde(rename = "parser.validateSkillMarkdown")]
    ValidateSkillMarkdown {
        markdown: String,
        #[serde(default)]
        mode: ParserSkillMode,
    },
    #[serde(rename = "parser.validateRunnerManifestYaml")]
    ValidateRunnerManifestYaml { yaml: String },
    #[serde(rename = "parser.validateGraphYaml")]
    ValidateGraphYaml { yaml: String },
    #[serde(rename = "parser.validateToolManifestJson")]
    ValidateToolManifestJson { json: String },
    #[serde(rename = "parser.validateSkillSource")]
    ValidateSkillSource {
        source: JsonObject,
        #[serde(default)]
        runx: Option<JsonObject>,
    },
    #[serde(rename = "parser.validateSkillArtifactContract")]
    ValidateSkillArtifactContract {
        #[serde(default)]
        artifacts: Option<JsonValue>,
        #[serde(default = "default_artifact_field")]
        field: String,
    },
    #[serde(rename = "parser.extractSkillQualityProfile")]
    ExtractSkillQualityProfile { body: String },
    #[serde(rename = "parser.resolvePostRunReflectPolicy")]
    ResolvePostRunReflectPolicy {
        #[serde(default)]
        runx: Option<JsonObject>,
        #[serde(default = "default_runx_field")]
        field: String,
    },
    #[serde(rename = "parser.validateSkillInstall")]
    ValidateSkillInstall {
        markdown: String,
        origin: SkillInstallOrigin,
    },
}

#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ParserSkillMode {
    #[default]
    Strict,
    Lenient,
}

impl From<ParserSkillMode> for ValidateSkillOptions {
    fn from(mode: ParserSkillMode) -> Self {
        match mode {
            ParserSkillMode::Strict => Self {
                mode: ValidateSkillMode::Strict,
            },
            ParserSkillMode::Lenient => Self {
                mode: ValidateSkillMode::Lenient,
            },
        }
    }
}

fn evaluate_parser_input(input: ParserDocument) -> Result<JsonValue, ParserEvalError> {
    match ParserInput::from(input) {
        ParserInput::ValidateSkillMarkdown { markdown, mode } => {
            let raw = parse_skill_markdown(&markdown)?;
            to_json_value(validate_skill_with_options(raw, mode.into())?)
        }
        ParserInput::ValidateRunnerManifestYaml { yaml } => {
            let raw = parse_runner_manifest_yaml(&yaml)?;
            to_json_value(validate_runner_manifest(raw)?)
        }
        ParserInput::ValidateGraphYaml { yaml } => {
            let raw = parse_graph_yaml(&yaml)?;
            to_json_value(validate_graph(raw)?)
        }
        ParserInput::ValidateToolManifestJson { json } => {
            let raw = parse_tool_manifest_json(&json)?;
            to_json_value(validate_tool_manifest(raw)?)
        }
        ParserInput::ValidateSkillSource { source, runx } => {
            to_json_value(validate_skill_source(&source, runx.as_ref())?)
        }
        ParserInput::ValidateSkillArtifactContract { artifacts, field } => to_json_value(
            validate_skill_artifact_contract(artifacts.as_ref(), &field)?,
        ),
        ParserInput::ExtractSkillQualityProfile { body } => {
            to_json_value(extract_skill_quality_profile(&body))
        }
        ParserInput::ResolvePostRunReflectPolicy { runx, field } => {
            to_json_value(resolve_post_run_reflect_policy(runx.as_ref(), &field)?)
        }
        ParserInput::ValidateSkillInstall { markdown, origin } => {
            to_json_value(validate_skill_install(&markdown, origin)?)
        }
    }
}

fn to_json_value<T: Serialize>(value: T) -> Result<JsonValue, ParserEvalError> {
    let serialized = serde_json::to_value(value)
        .map_err(|error| ParserEvalError::SerializeOutput(error.to_string()))?;
    serde_json::from_value(serialized)
        .map_err(|error| ParserEvalError::SerializeOutput(error.to_string()))
}

fn default_artifact_field() -> String {
    "runx.artifacts".to_owned()
}

fn default_runx_field() -> String {
    "runx".to_owned()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn evaluates_skill_markdown_validation() -> Result<(), String> {
        let output = evaluate_parser_document_str(
            r#"{
              "kind": "parser.validateSkillMarkdown",
              "markdown": "---\nname: parser-demo\n---\n# Parser Demo\n",
              "mode": "strict"
            }"#,
        )
        .map_err(|error| error.to_string())?;
        let ParserEvalOutput::Output { value } = output;
        let JsonValue::Object(skill) = value else {
            return Err("expected validated skill object".into());
        };
        assert_eq!(
            skill.get("name"),
            Some(&JsonValue::String("parser-demo".to_owned()))
        );
        Ok(())
    }

    #[test]
    fn rejects_unsupported_parser_kind_before_deserializing() -> Result<(), String> {
        let error = match evaluate_parser_document_str(r#"{"kind":"parser.unknown"}"#) {
            Ok(_) => return Err("unsupported parser kind must fail closed".into()),
            Err(error) => error,
        };
        assert_eq!(error.code(), "invalid_input");
        assert!(error.to_string().contains("unsupported parser input kind"));
        Ok(())
    }
}