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}