fix(cli): handle skill: wrapper key in agents skill add YAML config #1472 #1506

Merged
HAL9000 merged 4 commits from fix/skill-add-yaml-wrapper-key into master 2026-05-30 08:32:23 +00:00
4 changed files with 213 additions and 1 deletions
+60
View File
@@ -310,3 +310,63 @@ Feature: Skill YAML schema validation
And the skill config model_dump should contain key "mcp_servers"
And the skill config model_dump should contain key "agent_skill_folders"
# ────────────────────────────────────────────────────────────
# skill: wrapper key scenarios
# ────────────────────────────────────────────────────────────
@tdd_issue
@tdd_issue_1472
Scenario: Load spec-compliant YAML with skill: wrapper key
Given a spec-compliant skill YAML with skill: wrapper key
When I validate the skill schema
Then the skill schema validation should succeed
And the skill config name should be "local/wrapped-skill"
And the skill config should have 1 tools
@tdd_issue
@tdd_issue_1472
Scenario: Load spec-compliant YAML with cleveragents: header and skill: wrapper
Given a spec-compliant skill YAML with cleveragents: header and skill: wrapper
When I validate the skill schema
Then the skill schema validation should succeed
And the skill config name should be "local/wrapped-with-meta"
@tdd_issue
@tdd_issue_1472
Scenario: skill: wrapper key with None value raises ValueError
Given a skill YAML with skill: wrapper key with None value
When I validate the skill schema expecting failure
Then the skill schema validation should fail
And the skill schema error should mention "empty"
@tdd_issue
@tdd_issue_1472
Scenario: skill: wrapper key with non-dict value raises ValueError
Given a skill YAML with skill: wrapper key and string value
When I validate the skill schema expecting failure
Then the skill schema validation should fail
And the skill schema error should mention "mapping"
@tdd_issue
@tdd_issue_1472
Scenario: skill: wrapper key with list value raises ValueError
Given a skill YAML with skill: wrapper key and list value
When I validate the skill schema expecting failure
Then the skill schema validation should fail
And the skill schema error should mention "mapping"
@tdd_issue
@tdd_issue_1472
Scenario: Flat YAML without wrapper key still works (backward compatibility)
Given a skill YAML string with only the name field
When I validate the skill schema
Then the skill schema validation should succeed
And the skill config name should be "local/empty-skill"
@tdd_issue
@tdd_issue_1472
Scenario: cleveragents: header alone (flat format with metadata, no skill: wrapper)
Given a skill YAML with cleveragents: header and flat format
When I validate the skill schema
Then the skill schema validation should succeed
And the skill config name should be "local/flat-with-meta"
+79
View File
@@ -651,3 +651,82 @@ def step_then_model_dump_contains_key(context: Context, key: str) -> None:
assert context.skill_config is not None
data: dict[str, Any] = context.skill_config.model_dump()
assert key in data, f"Key '{key}' not found in model_dump: {list(data.keys())}"
# ────────────────────────────────────────────────────────────
# skill: wrapper key test fixtures and steps
# ────────────────────────────────────────────────────────────
_SPEC_COMPLIANT_WRAPPED_YAML = """\
cleveragents:
version: "1.0"
skill:
name: local/wrapped-skill
tools:
- name: builtin/shell_execute
"""
_SPEC_COMPLIANT_WRAPPED_META_YAML = """\
cleveragents:
version: "2.0"
skill:
name: local/wrapped-with-meta
description: "A wrapped skill with metadata"
"""
_SKILL_WRAPPER_NONE_YAML = """\
skill:
"""
_SKILL_WRAPPER_STRING_YAML = """\
skill: "just a string"
"""
_SKILL_WRAPPER_LIST_YAML = """\
skill:
- item1
- item2
"""
_CLEVERAGENTS_FLAT_META_YAML = """\
cleveragents:
version: "1.0"
name: local/flat-with-meta
description: "Flat config with stray metadata"
"""
@given("a spec-compliant skill YAML with skill: wrapper key")
def step_given_spec_compliant_wrapper(context: Context) -> None:
"""Provide a spec-compliant YAML with cleveragents: header and skill: wrapper."""
context.skill_yaml_string = _SPEC_COMPLIANT_WRAPPED_YAML
@given("a spec-compliant skill YAML with cleveragents: header and skill: wrapper")
def step_given_spec_compliant_with_meta(context: Context) -> None:
"""Provide a spec-compliant YAML with both cleveragents: header and skill: wrapper."""
context.skill_yaml_string = _SPEC_COMPLIANT_WRAPPED_META_YAML
@given("a skill YAML with skill: wrapper key with None value")
def step_given_skill_wrapper_none(context: Context) -> None:
"""Provide a YAML with skill: wrapper key but no value (None)."""
context.skill_yaml_string = _SKILL_WRAPPER_NONE_YAML
@given("a skill YAML with skill: wrapper key and string value")
def step_given_skill_wrapper_string(context: Context) -> None:
"""Provide a YAML with skill: wrapper key but a non-dict string value."""
context.skill_yaml_string = _SKILL_WRAPPER_STRING_YAML
@given("a skill YAML with skill: wrapper key and list value")
def step_given_skill_wrapper_list(context: Context) -> None:
"""Provide a YAML with skill: wrapper key but a non-dict list value."""
context.skill_yaml_string = _SKILL_WRAPPER_LIST_YAML
@given("a skill YAML with cleveragents: header and flat format")
def step_given_cleveragents_flat_meta(context: Context) -> None:
"""Provide a flat-format YAML with cleveragents: metadata but no skill: wrapper."""
context.skill_yaml_string = _CLEVERAGENTS_FLAT_META_YAML
+18
View File
@@ -49,3 +49,21 @@ Reject Invalid Skill YAML
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} skill-schema-expected-fail
Validate Spec-Compliant Skill YAML With skill: Wrapper Key
[Tags] tdd_issue tdd_issue_1472
[Documentation] Parse a spec-compliant YAML with cleveragents: header and skill: wrapper key
${wrapped_yaml}= Set Variable ${TEMPDIR}${/}wrapped_skill.yaml
${yaml_content}= Catenate SEPARATOR=\n
... cleveragents:
... ${SPACE}${SPACE}version: "1.0"
... skill:
... ${SPACE}${SPACE}name: local/wrapped-skill
... ${SPACE}${SPACE}description: "A wrapped skill"
...
Create File ${wrapped_yaml} ${yaml_content}
${result}= Run Process ${PYTHON} ${HELPER} validate ${wrapped_yaml} cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} skill-schema-ok
+56 -1
View File
@@ -6,6 +6,8 @@ Provides :class:`SkillConfigSchema`, a Pydantic model that:
* Normalizes camelCase keys to snake_case before validation.
* Interpolates ``${ENV_VAR}`` placeholders from environment variables.
* Produces clear, actionable error messages for every validation failure.
* Handles the spec-required ``skill:`` wrapper key and ``cleveragents:``
metadata block for forward-compatible YAML parsing.
Schema definition lives in ``docs/schema/skill.schema.yaml``.
Example configs live under ``examples/skills/``.
@@ -392,6 +394,30 @@ class SkillConfigSchema(BaseModel):
def from_yaml(cls, yaml_string: str) -> SkillConfigSchema:
"""Parse and validate a skill YAML string.
Handles both the spec-required YAML format with a top-level
``skill:`` wrapper key and optional ``cleveragents:`` metadata
block, as well as the legacy flat format for backward
compatibility.
Spec-compliant YAML:
.. code-block:: yaml
cleveragents:
version: "1.0"
skill:
name: local/my-skill
tools:
- name: builtin/shell_execute
Flat (legacy) YAML, also supported:
.. code-block:: yaml
name: local/my-skill
tools:
- name: builtin/shell_execute
Args:
yaml_string: Raw YAML content.
@@ -399,7 +425,8 @@ class SkillConfigSchema(BaseModel):
Validated ``SkillConfigSchema`` instance.
Raises:
ValueError: If the YAML is not a mapping or is empty.
ValueError: If the YAML is not a mapping or is empty,
or if ``skill:`` has an invalid value.
pydantic.ValidationError: If schema validation fails.
"""
if yaml_string is None:
@@ -415,7 +442,35 @@ class SkillConfigSchema(BaseModel):
f"Skill YAML must be a mapping (key: value), got {type(raw).__name__}."
)
# ── Normalize camelCase keys ────────────────────────────
normalized = _normalize_keys(raw)
# ── Strip optional cleveragents metadata header ─────────
normalized.pop("cleveragents", None)
# ── Unwrap skill: wrapper key if present ────────────────
if "skill" in normalized:
wrapper = normalized.pop("skill")
if wrapper is None:
raise ValueError(
"skill: key is present but empty. "
"Provide a skill mapping with at least a 'name' field."
)
if not isinstance(wrapper, dict):
raise ValueError(
f"skill: key must contain a mapping (dict), "
f"got {type(wrapper).__name__}. "
"Wrap the skill configuration as: skill:\\n <your config here>"
)
# Merge wrapper content with normalized keys.
# Wrapper values take precedence so the spec-compliant
# format works reliably even if both formats overlap.
wrapper_dict: dict[str, Any] = wrapper
for key, value in wrapper_dict.items():
snake_key = _CAMEL_TO_SNAKE.get(key, key)
normalized[snake_key] = value
# ── Interpolate environment variables ───────────────────
interpolated = _interpolate_env_vars(normalized)
return cls.model_validate(interpolated)