fix(cli): handle skill: wrapper key in agents skill add YAML config #1472 #1506
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user