feat(skills): ship a ten-skill starter pack with a guide and honesty guards - #2697
feat(skills): ship a ten-skill starter pack with a guide and honesty guards#2697kovtcharov wants to merge 14 commits into
Conversation
…core Nothing in GAIA could read a SKILL.md before this. Agents gain capability only by shipping new Python, so a user who wants a new one has no path short of forking the agent — and the 800K+ skills already published to community registries were unreachable. Now a folder with a SKILL.md drops into ~/.gaia/skills/ and any agent can load it at runtime, and an existing Claude Code library under .claude/skills/ works unchanged. Phase 1 of #1019, deliberately scoped to keep a sandbox out of it: a skill may add instructions and reach connector-backed services, and a skill declaring a local-capability permission (filesystem/shell/database/desktop/env) is refused with an actionable error rather than loaded without enforcement. - gaia.skills.format: frontmatter + body -> Skill; round-trip is identity; unknown top-level keys and foreign metadata.<vendor> namespaces survive a write. compatibility/allowed-tools parse but are ignored — never a permission mechanism. - gaia.skills.manager: three v1 roots (agent-bundled -> ~/.gaia/skills -> read-only .claude/skills), first-wins precedence with the shadowed copy kept visible, progressive disclosure, FileWatcher hot-reload. - gaia.skills.loader: tools register under <skill>/<tool>; a manifest that contradicts tools.py fails loudly and restores the registry byte-for-byte. - gaia.skills.permissions: network/mcp resolve to the existing ConnectorRequirement — no second grant ledger. - Agent.load_skill/unload_skill, and gaia skill list|info|create|import|export. Closes #888
…s too The refusal only ran in Agent.load_skill, so a caller reaching for the lower-level register_skill_tools could hand a skill executable reach without passing the permission gate. Move it to the function that actually registers tools so the invariant holds at every entry point.
Five downstream issues build directly on load_skill / skill_manager / loaded_skills; the return types were inferred, not declared.
The unload test only checked the global registry. Assert on every surface the model can see — the instance snapshot and the rendered tool block — and add a reload-after-unload case so a poisoned module cache would fail. Also pin the lazily-built manager's user root at tmp_path so no test can resolve the real ~/.gaia/skills.
/skills/ was ignored wholesale for AMD-internal marketplace installs; narrow the rule so the shipped starter pack under skills/starter/ stays tracked.
Nine instruction-only recipe skills plus one tool-providing skill (rss-digest). Every manifest validates against the Phase 1 parser, round-trips byte-identical, and declares only connector-bridged permissions (network:*, mcp:connect).
Iterates skills/starter/ so a new skill is covered without a new test. Beyond parse+validate, asserts every tools_required name resolves to a tool a mixin really registers — the loader does not check that, so a typo would otherwise ship a skill whose procedure references a tool the model never has.
One section per shipped skill with what it does, how to configure, and how to install and run — plus the four proposed skills that are NOT shipped and the specific blocker for each, so the guide never promises more than the runtime delivers. Credits Nous Research / Hermes and agentskills.io.
…in it The snippet called a 'query' method ChatAgent does not have. Added a test that fails if the documented API drifts again.
Review found procedures that name real tools but call them in ways the tools reject: check-in passed category="task" (not a valid memory category, so remember errored and recall matched nothing), data-explore's example SQL omitted the scratch_ table prefix every query needs, and research-report told the model to write files without declaring write_file. Harden rss-digest's fetch_rss too: refuse feeds carrying a DTD (stdlib ElementTree expands entities), report unrecognized dialects instead of returning an empty digest that reads as 'nothing published', and close the WebClient session it opens. New guards check argument semantics, not just tool names, and pin the guide's per-skill tool lists to the manifests.
A windowed scan of the first 4 KB was escapable with a large leading comment. Scan the whole prolog instead, and stop at the root element so a <!DOCTYPE inside an entry's embedded HTML stays content rather than a false refusal.
|
Heads-up on what the green checks do and do not cover: the unit-test workflow did not run on this PR. Run locally instead: The three semantic guards were mutation-tested — each was confirmed to fail when the bug it targets is reintroduced, so they are real guards rather than tautologies. Retargeting to |
The base branch was changed.
… gate Skills are contributed by pull request — the hub's publish endpoint authenticates against PUBLISH_TOKENS, a maintainer-held wrangler secret, so there is no self-serve path and never was. That makes this workflow the primary contributor route rather than a secondary check, and it is now built to stop a merge: - BLOCK and unparseable fail outright, with no override. Sign-off is for the judgement call, never for a rejected skill. - REVIEW fails until a maintainer applies 'skill-audit-reviewed'. Applying the label re-runs the check automatically, and labels need repo write access, so the sign-off is both auditable and maintainer-only. - The paths filter is gone. A required check that never runs sits pending forever, so a path filter would have made every non-skill PR unmergeable; the job runs everywhere and no-ops in ~15s instead. Sign-off unblocks the merge only: the verdict on record stays REVIEW, publishing still refuses it, and merging is not a tier promotion. The tier stays owned by the audit verdict plus signing, which is why the skills route deliberately diverges from the agent route where review sets the trust level. Contributions land in skills/community/<name>/, a sibling lane to #2697's skills/starter/ rather than a third layout — starter/ is AMD-authored material that exists to be copied, and a contributed skill must not be presented to newcomers as a reference implementation nobody reviewed as one. A contract test locks the gate's load-bearing settings, because the failure mode is silent: reintroduce a paths filter or soften REVIEW to a warning and the workflow still runs and still goes green, it just stops gating anything.
Verdict: Request changesThis PR ships the Phase 1 skills runtime ( 🔒 SECURITY CONCERN: entity-expansion (billion-laughs) DoS guard is bypassable in the Real-world evidence
🔍 Technical details🔴 / 🔒 Prolog DTD-guard bypass —
|
|
The SD check was an infra hiccup, not related to this change — re-ran it and it's green. For the record: the models were already cached, so this wasn't a slow download. The "Initialize SD Profile" step hung for its full 10m timeout inside model verification (loading the models for a warm-up inference), which left too little of the 15m job budget for the tests and the run got cancelled. On the re-run the same step took 47s and the whole job finished in 1m37s — so the timeout budget is fine and needs no change; it was a one-off stall, most likely contention with another job on the shared Lemonade runner. |
GAIA could parse and validate a
SKILL.mdafter #2669, but there was nothing to actually look at — a new user runninggaia skill listsaw an empty table and a format spec. This ships ten worked skills underskills/starter/that install, inspect, and fork today, covering web research, RAG, scratchpad SQL, memory, a connector-bridged MCP permission, and a skill that contributes its own@tool. Each one is a template to copy rather than a feature to use, which is the point: the pack argues GAIA does whatever you can describe, not ten fixed things.Four of the ten skills proposed in #893 are deliberately not here. They describe capabilities GAIA does not have yet, and a skill that parses but cannot run is worse than a missing one:
morning-briefgaia schedule add --skillis rejected at add time; no Gmail/Calendar/Weather connector in the catalog; TTS is not an agent tool; the Telegram adapter has no voice-note path.daily-briefships the composition half.voice-researchgaia talk).trading-alertemail-triagegmailconnector id — Gmail is a scope of thegoogleOAuth connector. Email triage already ships as a full agent (gaia email);github-triagecovers the same connector-bridged primitive.Three more (
source-watch,price-watch,check-in) ship rescoped to on-demand, because the scheduler cannot run skills — each says so in its own body rather than only in the guide. Scheduling is the single largest unblock. The guide carries the same table so the docs never promise more than the runtime delivers, and credits Nous Research / Hermes for the starter-pack model and agentskills.io for the format.Based on #2669 (
claudia/task-7afdc4b3), notmain— retarget once that merges. Separately filed #2687 for a pre-existing IPv6 bug inWebClientfound while validating the web-facing skills; left out of this PR to keep it scope-clean.Test plan
python -m pytest tests/unit/test_starter_skills.py -q— 120 passed, 9 skipped. Iteratesskills/starter/so a new skill is covered without a new test.network:*/mcp:connect:<id>—mcp:connectresolved against the live connector catalog, so it cannot name a connector a user can't configure.tools_requiredname is checked against tools the real mixin registrars produce (the Phase 1 loader does not check this, so a typo would otherwise ship silently).category=literals againstVALID_CATEGORIES, and scratchpad SQL for the mandatoryscratch_prefix. Both guards were confirmed to fail on the bugs they catch.HOME/GAIA_CONFIG_DIRredirected intotmp_path:gaia skill import skills/starter/<name>→gaia skill list --json→gaia skill info <name> --jsonfor all ten.source-watch, rename the directory +name, import — verified end to end. A fork that forgets to rename fails loudly.rss-digest/fetch_rssrun against real Atom (github.com/amd/gaia/releases.atom) and RSS 2.0 (hnrss.org/frontpage) feeds; refuses DTD-bearing feeds, loopback addresses, and unrecognized dialects instead of returning an empty digest.python util/lint.py --allclean.Closes #893