Skip to main content

runx_parser/
skill.rs

1use runx_contracts::{ExecutionSemantics, JsonObject, JsonValue};
2
3use crate::{ValidationError, json_fields::JsonFieldReader};
4
5mod catalog;
6mod execution_semantics;
7mod fixtures;
8mod governance;
9mod markdown;
10mod runner_definition;
11mod sandbox;
12mod source;
13mod types;
14
15pub use catalog::{CatalogAudience, CatalogKind, CatalogMetadata, CatalogRole, CatalogVisibility};
16pub use fixtures::{
17    HarnessCallerFixture, HarnessExpectation, ReceiptExpectation, RunnerHarnessCase,
18    RunnerHarnessManifest,
19};
20pub use governance::validate_skill_artifact_contract;
21pub use markdown::{extract_skill_quality_profile, parse_skill_markdown};
22pub use source::validate_skill_source;
23pub use types::{
24    ActDeclaration, InputMode, RawSkillIr, SkillArtifactContract, SkillHttpSource,
25    SkillIdempotencyPolicy, SkillInput, SkillMcpServer, SkillQualityProfile, SkillRetryPolicy,
26    SkillRunnerDefinition, SkillSandbox, SkillSource, SourceKind, ValidateSkillMode,
27    ValidateSkillOptions, ValidatedSkill,
28};
29
30pub(crate) use catalog::validate_catalog_metadata;
31pub(crate) use fixtures::validate_harness_manifest;
32pub(crate) use runner_definition::validate_runner_definition;
33
34use execution_semantics::validate_execution_semantics;
35use governance::validate_skill_governance;
36use governance::{
37    validate_allowed_tools, validate_artifact_contract, validate_idempotency, validate_inputs,
38    validate_mutating, validate_retry,
39};
40use sandbox::validate_sandbox;
41use source::default_agent_source;
42use source::validate_source;
43use source::validate_source_fields;
44
45const FIELDS: JsonFieldReader = JsonFieldReader::new("skill");
46
47pub(super) use crate::json_fields::{field_value, first_value, nested_value};
48
49struct SkillGovernance {
50    retry: Option<SkillRetryPolicy>,
51    idempotency: Option<SkillIdempotencyPolicy>,
52    mutating: Option<bool>,
53    artifacts: Option<SkillArtifactContract>,
54    allowed_tools: Option<Vec<String>>,
55    execution: Option<ExecutionSemantics>,
56}
57
58pub fn validate_skill(raw: RawSkillIr) -> Result<ValidatedSkill, ValidationError> {
59    validate_skill_with_options(raw, ValidateSkillOptions::default())
60}
61
62pub fn validate_skill_with_options(
63    raw: RawSkillIr,
64    options: ValidateSkillOptions,
65) -> Result<ValidatedSkill, ValidationError> {
66    let runx = validate_runx_metadata(raw.frontmatter.get("runx"), options.mode)?;
67    let source = raw
68        .frontmatter
69        .get("source")
70        .map(|value| FIELDS.optional_object(Some(value), "source"))
71        .transpose()?
72        .flatten()
73        .unwrap_or_else(default_agent_source);
74    let risk = raw.frontmatter.get("risk").cloned();
75    let governance = validate_skill_governance(&raw, runx.as_ref(), risk.as_ref())?;
76    let category = validate_portable_skill_category(&raw)?;
77    let runx_category = validate_runx_skill_category(runx.as_ref())?;
78
79    Ok(ValidatedSkill {
80        name: FIELDS.required_string(raw.frontmatter.get("name"), "name")?,
81        description: FIELDS.optional_string(raw.frontmatter.get("description"), "description")?,
82        category,
83        runx_category,
84        body: raw.body.clone(),
85        source: validate_source(&source, runx.as_ref())?,
86        inputs: validate_inputs(
87            FIELDS
88                .optional_object(raw.frontmatter.get("inputs"), "inputs")?
89                .unwrap_or_default(),
90        )?,
91        auth: raw.frontmatter.get("auth").cloned(),
92        risk: risk.clone(),
93        runtime: raw.frontmatter.get("runtime").cloned(),
94        retry: governance.retry,
95        idempotency: governance.idempotency,
96        mutating: governance.mutating,
97        artifacts: governance.artifacts,
98        quality_profile: extract_skill_quality_profile(&raw.body),
99        allowed_tools: governance.allowed_tools,
100        execution: governance.execution,
101        runx,
102        raw,
103    })
104}
105
106fn validate_portable_skill_category(raw: &RawSkillIr) -> Result<Option<String>, ValidationError> {
107    Ok(normalize_optional_category(FIELDS.optional_string(
108        raw.frontmatter.get("category"),
109        "category",
110    )?))
111}
112
113fn validate_runx_skill_category(
114    runx: Option<&JsonObject>,
115) -> Result<Option<String>, ValidationError> {
116    Ok(normalize_optional_category(FIELDS.optional_string(
117        field_value(runx, "category"),
118        "runx.category",
119    )?))
120}
121
122fn normalize_optional_category(value: Option<String>) -> Option<String> {
123    value
124        .map(|value| value.trim().to_owned())
125        .filter(|value| !value.is_empty())
126}
127
128fn validate_runx_metadata(
129    value: Option<&JsonValue>,
130    mode: ValidateSkillMode,
131) -> Result<Option<JsonObject>, ValidationError> {
132    match value {
133        None | Some(JsonValue::Null) => Ok(None),
134        Some(JsonValue::Object(value)) => Ok(Some(value.clone())),
135        Some(_) if mode == ValidateSkillMode::Lenient => Ok(None),
136        Some(_) => Err(ValidationError::InvalidField {
137            field: "runx".to_owned(),
138            message: "runx must be an object when present.".to_owned(),
139        }),
140    }
141}