-
Notifications
You must be signed in to change notification settings - Fork 3.9k
150 lines (134 loc) · 5.6 KB
/
Copy pathskill-spec-validation.yml
File metadata and controls
150 lines (134 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
name: Skill Spec Validation
on:
pull_request:
paths:
- "skills/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/skill-spec-validation.yml"
push:
branches:
- main
paths:
- "skills/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: skill-spec-validation-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
name: Validate skills against the Agent Skills spec
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v8.0.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
python-version: "3.13"
- name: Install dependencies
run: uv sync --python 3.13
# The reference validator from https://agentskills.io/specification. It checks the
# closed set of allowed frontmatter fields, name rules (incl. directory match),
# description/compatibility length limits, and parses frontmatter with strictyaml
# -- which rejects JSON-style flow mappings such as `metadata: {"version": "1.0"}`.
- name: skills-ref validate
run: |
set -uo pipefail
fail=0
for d in skills/*/; do
if ! out=$(uv run skills-ref validate "$d" 2>&1); then
fail=1
echo "::error file=${d}SKILL.md::$(echo "$out" | tail -n +2 | tr '\n' ' ')"
echo "FAIL $d"
echo "$out" | sed 's/^/ /'
fi
done
echo "Validated $(ls -d skills/*/ | wc -l) skills."
exit $fail
# Rules the reference validator does not enforce: this repo's metadata.version
# requirement (see AGENTS.md), plus spec constraints skills-ref accepts but the
# spec text requires -- allowed-tools must be a space-separated string, and
# metadata values must be strings apart from the host-manifest blocks that have
# to stay nested objects (see NESTED_OK below).
- name: Repo and spec rules skills-ref does not check
run: |
uv run --with pyyaml python - <<'PY'
import re
import sys
from pathlib import Path
import yaml
# Host manifest blocks that must stay nested mappings. OpenClaw's
# resolveOpenClawManifestBlock() requires `typeof candidate === "object"`, so
# encoding these as JSON strings silently disables its gating and credential
# injection. Nested mappings still pass `skills-ref validate`.
NESTED_OK = {"openclaw", "hermes"}
# Requires the closing delimiter on its own line. A naive split("---") would
# happily re-split at a `---` accidentally glued to the last frontmatter value.
FM_RE = re.compile(r"\A---\n(.*?)\n---\n", re.S)
errors, warnings = [], []
for d in sorted(Path("skills").iterdir()):
if not d.is_dir():
continue
md = d / "SKILL.md"
if not md.exists():
errors.append(f"{d}: missing SKILL.md")
continue
text = md.read_text()
m_fm = FM_RE.match(text)
if not m_fm:
errors.append(
f"{md}: frontmatter must open with `---` and close with `---` "
f"on its own line"
)
continue
fm = yaml.safe_load(m_fm.group(1))
at = fm.get("allowed-tools")
if at is not None:
if not isinstance(at, str):
errors.append(
f"{md}: allowed-tools must be a space-separated string, "
f"got {type(at).__name__}"
)
elif "," in at:
errors.append(
f"{md}: allowed-tools must be space-separated, not "
f"comma-separated: {at!r}"
)
m = fm.get("metadata")
if not isinstance(m, dict):
errors.append(f"{md}: missing a `metadata` mapping (see AGENTS.md)")
else:
if "version" not in m:
errors.append(f"{md}: metadata.version is required (see AGENTS.md)")
for k, v in m.items():
if k in NESTED_OK:
if not isinstance(v, dict):
errors.append(
f"{md}: metadata.{k} must stay a nested mapping, got "
f"{type(v).__name__} -- a JSON string silently disables "
f"host gating and credential injection"
)
continue
if isinstance(v, str):
continue
errors.append(
f"{md}: metadata.{k} must be a string, got {type(v).__name__} "
f"-- quote it (versions and dates especially)"
)
lines = text.count("\n") + 1
if lines > 500:
warnings.append(f"{md}: {lines} lines; the spec recommends under 500")
for w in warnings:
print(f"::warning file={w.split(':')[0]}::{w}")
for e in errors:
print(f"::error file={e.split(':')[0]}::{e}")
print(f"FAIL {e}")
print(f"\n{len(errors)} error(s), {len(warnings)} warning(s).")
sys.exit(1 if errors else 0)
PY