runx-parser 0.6.19

Pure Rust parser parity crate for runx skills, graphs, and tool manifests.
Documentation
use runx_contracts::{ExecutionSemantics, JsonObject, JsonValue};

use crate::{ValidationError, json_fields::JsonFieldReader};

mod catalog;
mod execution_semantics;
mod fixtures;
mod governance;
mod markdown;
mod runner_definition;
mod sandbox;
mod source;
mod types;

pub use catalog::{CatalogAudience, CatalogKind, CatalogMetadata, CatalogRole, CatalogVisibility};
pub use fixtures::{
    HarnessCallerFixture, HarnessExpectation, ReceiptExpectation, RunnerHarnessCase,
    RunnerHarnessManifest,
};
pub use governance::validate_skill_artifact_contract;
pub use markdown::{extract_skill_quality_profile, parse_skill_markdown};
pub use source::validate_skill_source;
pub use types::{
    ActDeclaration, InputMode, RawSkillIr, SkillArtifactContract, SkillHttpSource,
    SkillIdempotencyPolicy, SkillInput, SkillMcpServer, SkillQualityProfile, SkillRetryPolicy,
    SkillRunnerDefinition, SkillSandbox, SkillSource, SourceKind, ValidateSkillMode,
    ValidateSkillOptions, ValidatedSkill,
};

pub(crate) use catalog::validate_catalog_metadata;
pub(crate) use fixtures::validate_harness_manifest;
pub(crate) use runner_definition::validate_runner_definition;

use execution_semantics::validate_execution_semantics;
use governance::validate_skill_governance;
use governance::{
    validate_allowed_tools, validate_artifact_contract, validate_idempotency, validate_inputs,
    validate_mutating, validate_retry,
};
use sandbox::validate_sandbox;
use source::default_agent_source;
use source::validate_source;
use source::validate_source_fields;

const FIELDS: JsonFieldReader = JsonFieldReader::new("skill");

pub(super) use crate::json_fields::{field_value, first_value, nested_value};

struct SkillGovernance {
    retry: Option<SkillRetryPolicy>,
    idempotency: Option<SkillIdempotencyPolicy>,
    mutating: Option<bool>,
    artifacts: Option<SkillArtifactContract>,
    allowed_tools: Option<Vec<String>>,
    execution: Option<ExecutionSemantics>,
}

pub fn validate_skill(raw: RawSkillIr) -> Result<ValidatedSkill, ValidationError> {
    validate_skill_with_options(raw, ValidateSkillOptions::default())
}

pub fn validate_skill_with_options(
    raw: RawSkillIr,
    options: ValidateSkillOptions,
) -> Result<ValidatedSkill, ValidationError> {
    let runx = validate_runx_metadata(raw.frontmatter.get("runx"), options.mode)?;
    let source = raw
        .frontmatter
        .get("source")
        .map(|value| FIELDS.optional_object(Some(value), "source"))
        .transpose()?
        .flatten()
        .unwrap_or_else(default_agent_source);
    let risk = raw.frontmatter.get("risk").cloned();
    let governance = validate_skill_governance(&raw, runx.as_ref(), risk.as_ref())?;
    let category = validate_portable_skill_category(&raw)?;
    let runx_category = validate_runx_skill_category(runx.as_ref())?;

    Ok(ValidatedSkill {
        name: FIELDS.required_string(raw.frontmatter.get("name"), "name")?,
        description: FIELDS.optional_string(raw.frontmatter.get("description"), "description")?,
        category,
        runx_category,
        body: raw.body.clone(),
        source: validate_source(&source, runx.as_ref())?,
        inputs: validate_inputs(
            FIELDS
                .optional_object(raw.frontmatter.get("inputs"), "inputs")?
                .unwrap_or_default(),
        )?,
        auth: raw.frontmatter.get("auth").cloned(),
        risk: risk.clone(),
        runtime: raw.frontmatter.get("runtime").cloned(),
        retry: governance.retry,
        idempotency: governance.idempotency,
        mutating: governance.mutating,
        artifacts: governance.artifacts,
        quality_profile: extract_skill_quality_profile(&raw.body),
        allowed_tools: governance.allowed_tools,
        execution: governance.execution,
        runx,
        raw,
    })
}

fn validate_portable_skill_category(raw: &RawSkillIr) -> Result<Option<String>, ValidationError> {
    Ok(normalize_optional_category(FIELDS.optional_string(
        raw.frontmatter.get("category"),
        "category",
    )?))
}

fn validate_runx_skill_category(
    runx: Option<&JsonObject>,
) -> Result<Option<String>, ValidationError> {
    Ok(normalize_optional_category(FIELDS.optional_string(
        field_value(runx, "category"),
        "runx.category",
    )?))
}

fn normalize_optional_category(value: Option<String>) -> Option<String> {
    value
        .map(|value| value.trim().to_owned())
        .filter(|value| !value.is_empty())
}

fn validate_runx_metadata(
    value: Option<&JsonValue>,
    mode: ValidateSkillMode,
) -> Result<Option<JsonObject>, ValidationError> {
    match value {
        None | Some(JsonValue::Null) => Ok(None),
        Some(JsonValue::Object(value)) => Ok(Some(value.clone())),
        Some(_) if mode == ValidateSkillMode::Lenient => Ok(None),
        Some(_) => Err(ValidationError::InvalidField {
            field: "runx".to_owned(),
            message: "runx must be an object when present.".to_owned(),
        }),
    }
}