| title | Skill Format |
|---|---|
| description | SKILL.md field grammar — Agent Skills (agentskills.io) base + a metadata.gaia namespace, permission model, security tiers, and Hermes/OpenClaw compatibility |
| icon | puzzle-piece |
Also exists (hub side): workers/agent-hub/src/ — the skill catalog lane, the skills/ R2 namespace, POST /publish/skill validating this grammar server-side, and the pre-publish audit gate (#2467).
Still proposed (not written yet): the /api/skills router and Agent UI panel, gaia skill update, and the permission sandbox for local-capability domains (#1019). Every such symbol is marked PROPOSED where it appears.
Shipped since: the marketplace verbs gaia skill search|install|remove|publish plus keygen/trust, install-time tier-ceiling enforcement, and the declarative skills: block (#2467); plus gaia skill migrate (#692).
Module: gaia.skills — shipped (#888). The parser, validator, discovery, tool loader, permission grammar, and gaia skill list|info|create|import|export all exist. The contract is grounded on the live tool registry in gaia.agents.base.tools.
Status: Format = decided. Runtime = Phase 1 + the marketplace shipped (loader, CLI, publish/install, signature verification, install-time tier ceilings, declarative skills: / skill_sets:); the local-capability sandbox and the REST/UI surface remain proposed. The companion Agent Skills spec owns the integration surface (discovery, scoping, progressive disclosure); this document is authoritative for the field grammar.
Target consumers: #887 (skill auto-synthesis), #553 (self-improving agent), #1451 (tool-loader Part 3), #647 (skill marketplace), #648 (OEM bundling), #462 (Agent Manifest).
A GAIA agent's capabilities are compiled in. Tools arrive through hardcoded
mixins (RAGToolsMixin, BrowserToolsMixin, … catalogued in KNOWN_TOOLS,
registry.py:38)
wired into the agent class and shipped inside the wheel. A user who wants a new
capability has no path short of forking the agent. Skills make a capability
portable: a folder with a SKILL.md an agent loads only when relevant.
The Agent Skills spec answers why skills, and how an agent composes them. This document answers a narrower, higher-stakes question: what exactly goes in the frontmatter. That matters because the frontmatter is a contract, not a private detail — several landed and in-flight features read or emit these fields:
- #887 (skill auto-synthesis) and
#553 (self-improving agent) will
emit
SKILL.mdfiles via skill extraction. The field names they write must be the field names this spec defines. - #1451 (tool-loader Part 3)
reads the
tools_requiredfield to union a recipe's tools into per-turn tool selection — a hard cross-spec dependency already written down intool-loader.mdx. - #647 (marketplace) and #648 (OEM bundling) need a stable definition of "a skill" and its trust tiers to publish and pre-bundle.
So the single thing this spec must nail down is a frontmatter schema that is (a) the open standard, so the existing skill ecosystem works in GAIA unchanged; (b) a strict superset that carries GAIA's typed tools, permissions, and trust tiers; and (c) stable enough that the consumers above can build against it without churn.
GAIA does not invent a skill format. It adopts one and extends the frontmatter. The base and the compatible formats:
| Source | What it contributes |
|---|---|
| Agent Skills (used by Claude Code) | The base standard GAIA adopts: required name + description, optional license + metadata (an arbitrary map; "make key names reasonably unique"), a folder of SKILL.md + scripts//references//assets/, and progressive disclosure (metadata → body → resources). |
| Hermes Agent (Nous Research) | The metadata.hermes namespace pattern — vendor-specific fields nested under metadata.<vendor> so standard runtimes ignore them losslessly. |
| OpenClaw / ClawHub | The same metadata.openclaw namespace pattern, confirmed by the format owner. |
| Claude Code skills | The reference implementation of the Agent Skills standard; already credited in Agent Skills. |
GAIA's contribution is the metadata.gaia namespace: typed @tool
declarations, a <domain>:<level> permission grammar bridged to the connector
grant model, three install-time security tiers, and the tools_required recipe
contract — all expressed inside the standard's metadata map, so a GAIA skill
remains a valid standard skill.
The schema is the standard's required base plus a single GAIA-owned namespace.
A standard runtime reads the base and ignores metadata.gaia; GAIA reads both.
| Key | Standard | GAIA | Notes |
|---|---|---|---|
name |
required | required | ≤64 chars, lowercase alphanumeric + single hyphens, no leading/trailing/consecutive hyphen, must match the skill's directory name. |
description |
required | required | ≤1024 chars. The trigger signal the model reads to decide relevance. |
license |
optional | optional | Always MIT for GAIA skills (matches the repository license). |
metadata |
optional | optional | Arbitrary map. GAIA's fields live under metadata.gaia. |
version |
— | superset | Top-level. The standard has no top-level version; GAIA adds one (Hermes/OpenClaw also carry it). A tolerated superset key — standard runtimes ignore it. |
Everything GAIA-specific is nested here. Omit the whole block and the skill is a valid instruction-only skill (see Off-states).
metadata:
gaia:
security_tier: verified # verified | community | experimental (default: experimental)
permissions: # <domain>:<level>[:scope] — see Permission model
- network:read:*.brave.com
requirements: # all optional; defaults = no constraint
model: ">=7B" # minimum parameter count (advisory)
context: ">=8K" # minimum context window
python: ">=3.10"
dependencies: [requests>=2.31]
node_dependencies: ["@brave/brave-search-mcp-server"]
env_vars: [BRAVE_API_KEY]
hardware: { npu: optional, gpu_vram: ">=0GB" }
tools: # tools this skill PROVIDES (its own @tool fns)
- name: search_web
description: Search the web for current information
parameters:
query: { type: string, required: true }
max_results: { type: integer, required: false, default: 5 }
returns: { type: object }
atomic: false # maps to @tool(atomic=...)
tools_required: [] # registry tools this skill CONSUMES (recipe contract)
pairs_with: [] # skills this one is combined with (mutual; unknown names ignored)These are two different fields with two different consumers, and collapsing them breaks the recipe contract:
| Field | Meaning | Who reads it |
|---|---|---|
tools |
The skill's own @tool-decorated functions (provided capability). Maps to the @tool decorator / _TOOL_REGISTRY (tools.py:16, :19). |
The loader, to register and validate the skill's tools. |
tools_required |
Names of existing registry tools a recipe skill consumes (e.g. query_documents, read_file). The skill ships no code for them. |
#887 / #1451 — the tool-loader unions these into per-turn selection. |
The field name tools_required is locked as the cross-spec contract with
#887/#1451. A skill may have either, both, or neither.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Skill identifier; matches directory name. See naming. |
description |
string | yes | Trigger signal (≤1024 chars). |
license |
string | no | Always MIT for GAIA skills. |
version |
string | no (recommended) | SemVer. Top-level GAIA superset, recommended for publishing; 0.0.0 (unversioned) blocks publishing. |
metadata.gaia.security_tier |
string | no | verified | community | experimental. Default experimental. |
metadata.gaia.permissions |
list | no | <domain>:<level>[:scope] grants. Default: none. |
metadata.gaia.requirements |
object | no | Model / context / deps / env / hardware constraints. Default: none. |
metadata.gaia.tools |
list | no | Provided @tool declarations. Default: none (instruction-only). |
metadata.gaia.tools_required |
list | no | Registry tool names consumed (recipe contract). Default: []. |
metadata.gaia.pairs_with |
list | no | Skills this one is usually combined with on one request. Mutual, shown in the skill catalogue and named by load_skill; names that are not installed are ignored. See Companion skills. Default: []. |
The fields above describe one skill. An agent declares which skills it
composes in its gaia-agent.yaml, using three top-level keys
(#2466):
# Always-on: loaded on every launch, whichever set is active.
skills:
- mailbox-hygiene # plain-string form
- name: incident-review # mapping form
version: ">=0.1.0"
required: false
# Named, mutually-exclusive bundles. Exactly ONE is active per launch.
skill_sets:
personal: [inbox-triage, newsletter-digest]
work: [inbox-triage, meeting-scheduling]
# Required whenever skill_sets is non-empty.
default_skill_set: personal| Field | Type | Required | Description |
|---|---|---|---|
skills |
list | no | Always-on skill references. Default: []. |
skill_sets |
map | no | Set name → skill references. Default: {}. |
default_skill_set |
string | only when skill_sets is non-empty |
Which set applies when nothing selects one. |
Each entry in skills or a set's list is either a skill name or a mapping:
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | A skill name per naming. |
version |
string | no | SemVer or range, enforced at load against the installed skill's version — see below. |
required |
bool | no | Default true. A required: false skill that is missing or version-incompatible is logged and skipped; either failure on a required one fails the launch. |
Set names use the same slug shape as skill names, capped at 32 chars — a lowercase alphanumeric start and end with internal hyphens:
^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$
Sets overlap; they do not partition. The same skill may appear in several
sets — that is how a shared capability stays available across contexts. What a
set may not do is re-declare a skill already in the always-on skills: list;
that is a contradiction (an always-on skill loads for every set) and fails to
parse.
Every malformed shape fails at manifest-parse time with a message naming the
fix — a list that repeats a name, an empty set, a default_skill_set naming an
undeclared set, an unrecognized key in a reference mapping, a non-boolean
required. There is no partial acceptance: an agent either declares a coherent
set of skills or does not load.
What #2467 still owns is resolution across installable versions — choosing which version to fetch from the marketplace. Checking the one version actually installed happens today.
Selection semantics — the order a set is chosen in, the selector hook, and the
fail-loud behaviour on an undeclared name — belong to the integration surface and
live in Agent Skills → Skill sets. The grammar
above is implemented once, in
gaia/skills/sets.py,
and consumed by both the manifest validator and the runtime, so the two cannot
drift.
Skill names follow the Agent Skills rule: ^[a-z0-9]+(-[a-z0-9]+)*$, ≤64 chars,
and must equal the skill's directory name (so web-research/SKILL.md has
name: web-research). No leading, trailing, or consecutive hyphens. Names are
unique within a discovery root; precedence between roots is defined in
Agent Skills → Discovery.
Permissions follow <domain>:<level> with an optional :scope qualifier. There
are no implicit grants — a skill declares every domain it touches.
| Domain | Levels | Maps to | Runtime state |
|---|---|---|---|
network |
read, write, none |
Connector scope (see bridge below). | Bridged — resolves to a ConnectorRequirement |
mcp |
connect, none |
Connector scope (MCP-server connectors). | Bridged — resolves to a ConnectorRequirement |
filesystem |
read, write, none |
Local-capability domain (new; needs the skill sandbox). | Refused at load |
shell |
execute, none |
One named CLI, scoped: shell:execute:gh. |
Bridged — grants that binary; reads run, writes ask |
database |
read, write, none |
Local-capability domain (new). | Refused at load |
desktop |
control, none |
Local-capability domain (new). | Refused at load |
env |
read, none |
Environment beyond declared env_vars. |
Refused at load |
program |
run, none |
One named program the skill's own code starts: program:run:soffice. |
Bridged — through gaia.skills.context.run_program only; no shell, scrubbed environment |
scratch |
write, none |
A fresh temporary folder per tool call. Takes no scope. | Bridged — through gaia.skills.context.temp_dir and friends; deleted when the call returns |
Scopes narrow a grant: filesystem:read:~/.gaia/data/**,
network:read:*.brave.com, shell:execute:gh. An unscoped grant covers the
whole domain — except mcp:connect, shell:execute and program:run, where a
scope is mandatory (below). Grammar violations — an empty entry, a missing level, an
unknown domain, or a level the domain does not define — fail loudly at parse
time.
<domain>:none is inert in both directions: it produces no requirement and never
triggers a refusal, because an explicit denial asks for less than the default.
filesystem:none therefore loads fine.
Agent Skills already states that a skill carrying permissions is granted to an agent reusing the connectors per-agent grant model. This spec makes the mapping concrete rather than inventing a second grant system:
network:*andmcp:connectresolve, at scope-time, to aConnectorRequirement(connector_id, scopes, reason)(connectors/providers/base.py:23) — the same frozen primitive agents already declare viaREQUIRED_CONNECTORS(agent.py:297). The skill'spermissionsbecome the agent's connector requirements when the skill is loaded — appended to a per-instance copy, never to the ClassVar, so one agent's skill cannot leak requirements into a sibling.filesystem:*,database:*,desktop:*,env:*are new local-capability domains with no connector equivalent. Until the skill sandbox lands (PROPOSED — see Phased build) a skill declaring one is refused outright — at every security tier,verifiedincluded. A tier is a provenance claim, not a sandbox.shell:execute:<binary>has its own bridge — see Bring your own CLI. It is not routed through a connector and does not need the sandbox, because the grant is one named binary held to a three-tier subcommand table: reads run, a short list of writes asks the user per call, and every escalation class is refused outright.
Per-agent scoping reuses the namespaced_agent_id keying
(builtin: / custom:sha256: / installed:,
registry.py:392)
that already isolates agent grants today.
The bridge above did not say what happens when a permission's scope has no
counterpart in the connector catalog. The implementation
(permissions.py)
settled both cases, and these are now part of the contract:
mcp:connectmust name a catalog connector. A baremcp:connectis a validation error, not a wildcard — the error lists the available connector ids and points atgaia connectors list. So ismcp:connect:<unknown-id>. An unscoped MCP grant would mean "connect to any MCP server", which is exactly the ambient authority the permission model exists to prevent.- An unmatched
network:*scope resolves to a reservednetworkpseudo-connector-id. Nothing in the catalog represents raw outbound HTTP, sonetwork:read:*.brave.combecomesConnectorRequirement(connector_id="network", scopes=("read:*.brave.com",), …). If the scope does name a real catalog connector, the requirement targets that connector instead.
Some capabilities are a CLI, not a connector. GitHub triage does not need an MCP
server; it needs gh, which already owns its auth and its rate limits. A skill
declares the one binary it runs:
permissions:
- shell:execute:gh
tools_required:
- run_shell_commandLoading that skill grants gh only, to that skill's session, on that
agent instance — the grant lives on the agent (mirroring how skill-derived
connector requirements shadow the REQUIRED_CONNECTORS ClassVar), never in a
module-level allowlist, so it can never widen a sibling agent's shell.
unload_skill revokes it; a binary stays granted while another loaded skill
still declares it.
Four rules make the grant safe enough to bridge before the sandbox exists:
- The binary must be policed.
BINARY_POLICIES(skills/binaries.py) maps each declarable binary to its subcommand table. A binary with no entry cannot be declared — the refusal names what is declarable. - Subcommands are an allowlist, in three tiers. Reads run with no prompt:
for
gh,issue list|view|status,pr list|view|diff|checks|status,repo list|view,release list|view,run list|view,label list,search …,auth status, andgh apifor GET. A short list of writes is confirmable —issue create|comment|edit,pr comment,label create|edit— meaning the user is shown the exact command and answers yes / no / always-for-that-command-class. Everything absent is refused, sogh auth token(prints the credential),gh alias/gh extension/gh config/gh codespace(arbitrary code or a remote shell), and the irreversible verbs (pr merge,issue close,label delete,repo delete) are blocked without being enumerated — and are never offered as a prompt. Collapsing "refuse" into "ask" is what turns a gate into a habit; the tiers stay separate for that reason. - Flags that escalate are refused, even on a confirmable write.
gh apirejects-X/--methodwith anything butGET, rejects-f/-F/--field/--raw-field/--input(a body implies a write), and rejectsgraphql. A write rejects--body-file(uploads a local file's contents),--editor(hangs a stdin-less agent), and--web. The flag is checked before the action, so a write carrying one never reaches a prompt. - The binary must be installed, unless something substitutes for it.
load_skillchecksPATHand refuses when the binary is missing, because a skill that loads without the tool it documents produces confident answers from no data — the failure this bridge exists to prevent. Two things soften that:- A policy that names a
substitute(today onlypytest, which usually lives in a virtualenv rather than onPATH) loads without the grant, and its prompt says the command is unavailable and what to do instead — forpytest, callpytest.mainfromexecute_python_file. - A
BinarySetupentry on the policy lets the agent install the CLI and drive its browser sign-in itself, each behind its own approval prompt, so the refusal names an action instead of ending the conversation. See Setting up a CLI a skill needs.
- A policy that names a
The existing shell guardrails still apply on top: command chaining and
redirection (&&, ;, >, backticks, $()) are rejected before the policy
runs, and each segment of a pipeline is validated separately.
The grant replaces the per-call prompt for READS only.
run_shell_command is in Agent.confirmation_required_tools(), so every call
normally raises a confirmation modal. A triage is five to ten reads — five to ten
modals attended, and unattended a 100% failure rate, since a console with nobody
to ask denies. So a read fully covered by an active grant skips the prompt: the
user already consented, in front matter, to one binary and a read-only table. A
confirmable write does not: the grant declares which writes may be offered, and
the user still approves each one. Everything else is unchanged —
Agent._call_is_pre_authorized asks the host's skill_grant_covers_call hook,
which answers False unless every segment of the command is a granted binary
running an allowed read. pwd still prompts. So does gh issue list | head:
consent was for gh, not for a
pipeline. An agent that has loaded no such skill has no way to answer yes, and
its gate is byte-identical.
Adding a CLI is data plus a SKILL.md. Supporting GitLab is a "glab" entry
in BINARY_POLICIES and a skill that declares shell:execute:glab — no new
branch in the shell tool, no change to the permission grammar.
Three tiers set the install-time permission ceiling and grant behavior.
| Tier | Signing | At install | Ceiling (enforced) |
|---|---|---|---|
verified |
AMD-signed, audited | Auto-grant, no prompt | All declared permissions pre-approved. |
community |
Publisher-signed, automated scans | Dangerous grants prompt | network:read, network:write, mcp:connect, shell:execute. |
experimental |
None | Explicit opt-in (--allow-experimental) |
network:read only. |
Promotion path: experimental → community → verified. A publisher signs to
reach community; an AMD audit request reaches verified. Re-audit on every
version bump.
A claim is not a tier. gaia skill install computes
min(claimed tier, tier the signature earned) and enforces the ceiling against
that, so an unsigned skill can never install as verified no matter what its front
matter says. A key's trust-store role decides what it attests: publisher →
community, amd → verified. The trust store ships empty and no AMD key is
bundled, so out of the box every skill installs experimental — the honest floor
rather than a placeholder that makes the top tier look anchored. See
gaia skill install.
The dangerous set the spec names — shell:execute, desktop:control,
database:write — is retained as authoritative. shell:execute:<binary> is
reachable today through the policed binary bridge and prompts at community;
desktop:control and database:write are unbridged local capabilities, so they
are still refused before any tier check.
The install-time ceiling now ships (#2467): signatures are verified, the claimed tier is collapsed to the attested one, and a permission above the resulting ceiling refuses the install. What remains deferred to Phase 2 is the run-time sandbox for local-capability domains — which is why those domains are refused outright rather than granted at any tier.
**A `verified` stamp is provenance, not containment.** v1 skills are instruction + connector-bridged only. Do not read a tier as evidence that a sandbox is policing what the skill's code does at run time — no such sandbox exists yet ([#1019](#1019)).Every skill passes an automated audit before it can enter the hub catalog (#2468).
Skills are contributed by pull request — there is no self-serve publish path
(the hub's publish endpoint is gated on a maintainer-held token secret), so the
PR check is the gate: BLOCK fails it, REVIEW holds the PR for maintainer
sign-off, ALLOW passes. Submit under skills/community/<name>/; see
CLI Reference → Skills.
Authors run the same engine locally, so a clean local audit is the whole requirement:
gaia skill audit ./my-skill/ # ALLOW / REVIEW / BLOCK + file:line findings
gaia skill audit ./my-skill/ --tier community # would it clear the tier I want to claim?
gaia skill audit ./my-skill/ --json # the payload the publish path attachesExit codes are distinct so CI can branch: 0 allow, 5 review, 6 block, 4
the skill could not be parsed. A skill the audit cannot read is never passed —
there is nothing to audit and nothing to publish.
Four analyzers run over the directory. Nothing imports the skill, runs its code, or touches the network, so auditing a hostile skill is itself safe — this is a static gate, and the runtime sandbox remains Phase 2.
| Analyzer | What it looks for |
|---|---|
Code (tools.py, scripts/, any *.py) |
Dangerous constructs regardless of what was declared: eval/exec, shell=True, dynamic import, pickle.loads, native libraries, credential-file reads, bulk os.environ harvesting, encoded payloads fed to exec. AST-based, so os.system in a docstring is not a finding and from os import system as sh is. |
| Permission truth | Diffs the domains the code actually touches against metadata.gaia.permissions. An undeclared domain, an insufficient level (declared read, performs a write), use despite an explicit <domain>:none, or a write outside a declared scope are all hard findings — the declaration is what the consent prompt and the Phase 2 sandbox are built on. Over-declaration is advisory: asking for less than you declared cannot hurt the user. |
| Supply chain | Imports of packages the manifest never declares, dependencies fetched from outside a package index (git+, a URL, a path), and unpinned versions — an unpinned dependency means the code audited today is not the code that runs tomorrow. |
| Instruction body | Prompt-injection and jailbreak patterns in the Markdown body and the description. Both reach the model: the description from discovery onward, the body once the skill triggers. |
Tier-scaled rigor. The same findings produce different verdicts depending on the tier a skill claims, which is what makes the tiers above mean something at publish time:
| Claimed tier | Verdict |
|---|---|
experimental |
Advisory — findings are surfaced but do not gate. critical still blocks: there is no tier at which shipping known-malicious code is acceptable. |
community |
high forces REVIEW, critical blocks. |
verified |
medium forces REVIEW, high blocks — and the automated gate never grants it. A spotless skill claiming verified earns REVIEW: "cleared the robot, awaiting the human." |
A skill cannot be stamped a tier whose gate it did not clear. The report lists
the tiers its findings actually cleared, and the hub compares the claimed tier
against that list — so an unearned claim is refused, not recorded. verified
never appears in that list, which is what stops a publisher self-stamping the top
tier; the AMD audit is what adds it.
A new version re-earns its verdict. The report is bound to the skill name, the version, and a digest of the audited bytes, so a report cannot be replayed onto a different publish. CI re-runs the full audit on every PR touching a skill, with no "already audited" short-circuit.
**The gate proves consistency, not trustworthiness.** The audit report is a publisher-supplied form part with no attestation, so the binding checks close *replay and accident* — stale reports, forgotten re-audits, careless tier bumps — but not *forgery*. A hostile publisher can fabricate a report whose every field agrees, including the digest over their own files.That is why a recorded verdict says attestation: "publisher-asserted". Read it
as "self-consistent", never as "AMD vouches for this". Making a verdict
unforgeable needs an attestation the publisher cannot mint — report signing with
a CI-held key (#1710), or the hub
running the audit itself — not a stricter parse.
Disclosure. Findings name file:line and the fix, but the offending source
text is withheld from every report unless a caller passes --show-snippets
locally. In CI the per-finding detail goes to the repo's private Security → Code
scanning tab as SARIF; the PR comment carries verdicts, tiers, and counts only.
REVIEW currently refuses the publish rather than admitting it — the quarantine
lane and its maintainer sign-off workflow are
#2675.
A skill's tools map onto the existing decorator and registry. On load, the
runtime imports the skill's tools.py and registers each @tool under a
<skill-name>/<tool-name> namespace into _TOOL_REGISTRY to prevent collisions
when an agent composes multiple skills — the same namespacing
Agent Skills describes. The skill's
tools.py uses the standard @tool decorator unchanged:
# web-research/tools.py
from gaia.agents.base.tools import tool
@tool
def search_web(query: str, max_results: int = 5) -> dict:
"""Search the web for current information."""
...The manifest is the contract, in both directions. The loader rejects the skill — registering nothing — when any of these holds:
| Mismatch | Why it fails |
|---|---|
A declared tool has no @tool function |
The manifest advertises capability the code does not deliver. If the function exists but is undecorated, the error says so. |
tools.py registers a tool the manifest does not declare |
The manifest is what a user audits before granting; an undeclared tool is an unaudited one. |
| Parameter names differ from the signature | The error names which side each stray parameter came from. |
A parameter's required flag contradicts the signature's default |
Silent drift between docs and behavior. |
A parameter's declared type contradicts the annotation |
Only checked when the decorator could infer a type — an un-inferable annotation has nothing to contradict. |
tools declared but no tools.py on disk |
Either add the module or drop tools to make it instruction-only. |
tools.py fails to import |
The error names the exception and points at requirements.dependencies. |
Every failure restores _TOOL_REGISTRY (and sys.modules) byte-for-byte — there
is no partial load, as Agent Skills
requires. Shipping a tools.py while declaring no tools is not an error: the
module is simply not imported, and the skill loads instruction-only.
Shipped verbs (gaia/skills/cli.py) —
authoring and local management. Full flag reference in
CLI Reference → Skills.
gaia skill list [--json] [--root agent-bundled|user|claude-import]
gaia skill info <name> [--json] [--body]
gaia skill create <name> [--dir DIR] [--description TEXT] [--with-tools] [--force]
gaia skill import <folder|zip|https-url> [--name NAME] [--force] [--allow-experimental]
gaia skill export <name> [--output FILE.zip]
gaia skill audit <dir> [--json] [--output FILE] [--sarif FILE] [--path-prefix DIR]
[--tier TIER] [--fail-on SEVERITY] [--show-snippets]
gaia skill migrate <source> [--from openclaw|hermes|auto] [--out DIR] [--name NAME]
[--force] [--dry-run] [--json]Exit codes are distinct so scripts can branch: 0 ok, 2 usage, 3 not found,
4 invalid (including a discovery root containing a malformed skill), and — for
audit — 5 review, 6 block. REVIEW and BLOCK are separate codes so CI can
hold a skill for sign-off without reporting it as rejected.
Marketplace verbs (#2467 / #647) — these talk to the Agent Hub's skills lane:
gaia skill search "web search" [--json] [--hub-url URL]
gaia skill install <name[@semver-range]> [--allow-experimental] [--force] [--yes]
gaia skill remove <name>
gaia skill lock [--check|--relock] [--json]
gaia skill publish ./my-skill/ [--unsigned] [--audit-report FILE] [--dry-run]
gaia skill keygen [--key-name NAME] [--force]
gaia skill trust {list|add <key> [--role publisher|amd]|remove <key-id>}install accepts a SemVer range and takes the highest published version
satisfying it. A range admits a prerelease only if one of its targets names a
prerelease of the same major.minor.patch, such as >=1.3.0-rc.1. An omitted
version, *, or latest selects the highest stable release, or the highest
prerelease when no stable release exists. The same applies to any and an empty
range. Installs are recorded in
~/.gaia/skills/skill-lock.json with the requested range, the resolved version,
the artifact SHA-256, a digest of the installed files, the signing key, and the
tier actually installed at.
lock reads that record back: --check (the default) reports version, tier,
content, and presence drift between the lock and the disk, exiting 4 when
anything differs; --relock re-records the current state after an intentional
change. A community / verified skill whose files drifted refuses to load and
cannot be relocked — those bytes are not the ones its signature covered. Nor can
one with no digest recorded yet (an entry from a pre-digest build): stamping the
current bytes there would assert a signature nothing checked, so those reinstall
instead. An unreadable lock refuses every user-root skill, local ones
included — it cannot say which are attested.
Still PROPOSED — update (marketplace):
gaia skill update web-research # PROPOSEDThe Agent Hub Worker serves POST /publish/skill and a skill catalog lane
(#2467), and gaia skill publish is
the client that calls it.
That endpoint re-validates this grammar server-side before a skill may enter the catalog. It is a gatekeeper, not a replacement for the loader's validation above: a skill can be locally valid and still be refused publication. Rejections name the offending field.
nameanddescriptionare required;namematches^[a-z0-9]+(-[a-z0-9]+)*$and is ≤64 chars,descriptionis ≤1024.versionis required to publish (optional in the format itself, where a skill may be unversioned) and must be SemVer.0.0.0is refused — the unversioned sentinel can never become an immutable release.metadata.gaia.security_tiermust be one ofverified/community/experimental, and the tier is gated by the pre-publish security audit (#2468) —communityandverifiedcannot publish without a cleared report, and a skill cannot claim a tier whose gate it did not clear.permissions,tools,tools_required, andrequirementsmust match the shapes above; the catalog entry carries them so an installer can show the permission preview before anything is written to disk.
Skill names share one namespace with agent ids, so a name already taken by a published agent is refused (and vice versa).
See Agent Skills → Agent Hub integration for the catalog shape and the storage/route layout.
An agent names the skills it composes in its gaia-agent.yaml, and any Agent
subclass resolves that block at construction — so a custom agent under
~/.gaia/agents/<id>/ consumes an installed hub skill with no code change:
skills:
- name: web-research
version: ">=1.0.0"
required: true
- name: incident-review
version: ">=0.1.0"
required: false # optional enhancement; the agent runs without itResolution is local — gaia skill install puts the skill in a discovery root
and this reads what is there; an agent never pulls code from the network at
startup. Ordering mirrors agent dependencies:: a skill providing a tool another
declares in tools_required loads first, declaration order breaks ties, and a
cycle raises rather than picking a winner. A missing required: true skill raises
with the gaia skill install command that fixes it; a missing optional one is
logged and skipped.
The skill_sets: grammar is #2466. It
expands a set into SkillRequirement values and resolves them through the same
path, so there is one resolver and one place that enforces conflicts.
Acceptance #4 requires a skills management panel. The proposed /api/skills
router mirrors the existing /api/agents router
(ui/routers/agents.py:
list :130, detail :174, export :184, import :222) — the same
list/detail/install/remove shape, so the panel reuses the agents-panel
conventions rather than inventing new ones.
GET /api/skills # list installed skills (+ tier, version) PROPOSED
GET /api/skills/{name} # skill detail (manifest, permissions) PROPOSED
POST /api/skills/install # install from registry/path PROPOSED
DELETE /api/skills/{name} # remove PROPOSED
There is no /api/skills router today — the dashboard is greenfield. The data
it needs now exists (SkillManager.list_skills(), and gaia skill list --json /
gaia skill info --json already emit the list and detail shapes), so the router is
a thin wrapper rather than new machinery. Until it ships the panel is absent, not
broken (see Off-states).
Three formats, one namespace pattern. GAIA reads the standard base directly and treats Hermes/OpenClaw as migration sources, not runtime targets.
| Format | Where its fields live | GAIA support |
|---|---|---|
| Agent Skills (agentskills.io) | base + (GAIA) metadata.gaia |
Native, lossless. A bare standard skill loads unchanged. |
| Hermes (Nous Research) | metadata.hermes |
Migrate via gaia skill migrate --from hermes. |
| OpenClaw / ClawHub | metadata.openclaw |
Migrate via gaia skill migrate --from openclaw. |
A single shipped tool — gaia skill migrate --from {hermes,openclaw,auto}
(migrate.py,
#692) — reads the foreign namespace and
writes a metadata.gaia block. Every migrated skill lands experimental,
re-earning trust via the same reset_security_tier() gaia skill import uses.
Migration emits a report of fields needing manual review, and the output is
round-tripped through parse_skill() before install, so a migration can never
emit a SKILL.md the parser rejects.
Fields GAIA models fully are consumed — they move into metadata.gaia and
leave the vendor block. Anything partially or un-modeled is preserved under
metadata.<vendor> and named in the report, which is what makes the
vendor-namespace pattern lossless in practice rather than in principle.
| OpenClaw field | GAIA target | Disposition |
|---|---|---|
requires.env: [VAR] |
requirements.env_vars |
Consumed |
primaryEnv: VAR |
requirements.env_vars |
Consumed |
envVars: [{name|key, …}] |
requirements.env_vars (names) |
Preserved — per-variable descriptions are not modeled |
requires.tools: [t] |
tools_required |
Consumed |
requires.bins: [git] |
permissions: [shell:execute:git] |
Refused unless the binary is in BINARY_POLICIES |
requires.anyBins: [a, b] |
permissions: [shell:execute:a, …] |
Refused unless policed — and GAIA has no any-of form, so each is declared |
requires.config: [path] |
permissions: [filesystem:read:path] |
Refused — local capability |
install: [{node|uv|pip: pkg}] |
requirements.{node_,}dependencies |
Preserved — GAIA never runs install steps |
install: [{brew|go|shell: …}] |
— | Preserved + reported |
emoji, homepage, os, always, skillKey, nix, config |
— | Preserved + reported |
name (any case/underscores) |
name (lowercase-with-hyphens) |
Coerced + reported |
version: v2.1 |
version: 2.1.0 |
Coerced to SemVer + reported |
| Hermes field | GAIA target | Disposition |
|---|---|---|
requires.tools: [t] |
tools_required |
Consumed |
requires.env: [VAR] |
requirements.env_vars |
Consumed |
requires.bins / requires.config |
shell / filesystem permissions |
Refused — unpoliced binary / local capability |
category, tags, author |
— | Preserved + reported |
Validated against 26 real published ClawHub skills (each commit-pinned, with
its source URL, license, and digest recorded in
tests/fixtures/openclaw_skills/PROVENANCE.md).
The documented schema is not what the registry contains, so the migrator reads
all of these:
| Divergence seen in the wild | How migration handles it |
|---|---|
Vendor fields directly under metadata, no namespace key |
Detected by field shape and migrated from where they are |
| Vendor fields at the top level of the frontmatter | Same |
The namespace hoisted to a top-level clawdis: key |
Same |
Aliases metadata.clawdbot / metadata.clawdis |
Treated as openclaw |
| The same payload duplicated under two aliases | First-wins, with a note — merging would double-count every requirement |
| No frontmatter at all (~5% of the registry) | A per-skill blocker; the rest of the batch still migrates |
envVars entries keyed key rather than name |
Both read |
name disagreeing with its directory |
The migrated skill installs under its (coerced) name, satisfying GAIA's name-==-directory rule |
Of the 26, 13 migrate cleanly and 13 are refused — 10 for a local capability
(they shell out) and 3 for having no frontmatter. That ratio is the honest shape
of the corpus, not a shortfall: a skill whose whole purpose is running git or
tmux cannot be bridged until the sandbox lands.
- Agent Skills — owns the architecture and integration surface (discovery roots, progressive disclosure, scoping, the skill/tool/agent distinction). This doc owns the field grammar. Where they overlap, that spec is canonical for the compatibility surface and this one for the schema; the two are reconciled (see Correcting the prior spec).
- Agent Manifest (#462) — a
gaia-agent.yamlreferences skills by name + version range;SKILL.mdis the skill-level complement. The keys are specified in manifest-side grammar above; how a set is selected per launch is in Agent Skills → Skill sets. - Agent bundles — skill packaging relates to the existing agent bundle format
(
.zip+bundle.json,BUNDLE_FORMAT_VERSION = 1,export_import.py:40). - Builder template — scaffolds
agent.pytoday (builder/template.py); emittingSKILL.mdis a future extension, not current behavior. - OEM bundling (#648) — needs a
stable definition of "a skill" to pre-configure hardware SKUs. A SKU pre-bundles
skills as
verified-tier entries in a discovery root; the directory anatomy and tier defaults this doc fixes are that definition.
Six worked examples. The three for existing agents (acceptance #6) declare only
tool names that exist in the current registry. All parse as YAML; tool skills
round-trip against their tools.py.
Proves a bare Agent Skills SKILL.md loads with no GAIA fields — defaulting to
experimental, no tools, no permissions.
---
name: incident-review
description: Walk through a production incident postmortem. Use when the user mentions an outage, incident, or postmortem.
---
# Incident Review
1. Establish the timeline from first alert to resolution.
2. Separate root cause from contributing factors.
3. Draft action items with owners — never "the team".The restructured schema: standard base + metadata.gaia.
---
name: web-search
description: Search the web via the Brave Search API. Use when the user asks about current events or external facts.
license: MIT
version: 1.0.0
metadata:
gaia:
security_tier: verified
permissions:
- network:read:*.brave.com
requirements:
python: ">=3.10"
dependencies:
- requests>=2.31
env_vars:
- BRAVE_API_KEY
tools:
- name: search_web
description: Search the web for current information
parameters:
query: { type: string, required: true }
max_results: { type: integer, required: false, default: 5 }
returns: { type: object }
atomic: false
---
# Web Search
A self-contained Brave Search wrapper — `search_web` is this skill's own
`@tool`, backed by the skill's `tools.py`.A distilled procedure that consumes existing registry tools. Each
tools_required name resolves against a real registry tool (query_documents
from RAGToolsMixin, read_file from FileIOToolsMixin; remember from the
tool-loader CORE set).
---
name: triage-support-ticket
description: Triage an inbound support ticket end to end. Use when the user pastes a support ticket or asks to triage one.
version: 0.2.0
metadata:
gaia:
security_tier: experimental
tools_required:
- query_documents
- read_file
- remember
---
# Triage a Support Ticket
1. Pull matching policy docs with `query_documents`.
2. Read any attached log with `read_file`.
3. Record the disposition with `remember` for follow-up.Acceptance #6 — a skill mapping the rag mixin's real tools.
---
name: rag-search
description: Search and answer over indexed documents. Use when the user asks about the contents of uploaded files.
version: 1.0.0
metadata:
gaia:
security_tier: verified
permissions:
- filesystem:read
tools:
- name: query_documents
description: Answer a question over all indexed documents
parameters:
query: { type: string, required: true }
returns: { type: object }
- name: search_indexed_chunks
description: Return the top matching chunks for a pattern
parameters:
pattern: { type: string, required: true }
returns: { type: object }
- name: list_indexed_documents
description: List documents currently in the index
parameters: {}
returns: { type: object }
---
# RAG Search
Use `query_documents` for direct answers; `search_indexed_chunks` to inspect
evidence; `list_indexed_documents` to see what is available.Acceptance #6 — the browser mixin's real tools (search_web, fetch_page,
download_file).
---
name: web-research
description: Research the open web. Use when the user asks about current events or facts not in the documents.
version: 1.0.0
metadata:
gaia:
security_tier: verified
permissions:
- network:read
tools:
- name: search_web
description: Search the web for current information
parameters:
query: { type: string, required: true }
returns: { type: object }
- name: fetch_page
description: Fetch and extract the readable text of a URL
parameters:
url: { type: string, required: true }
returns: { type: object }
- name: download_file
description: Download a file from a URL to local disk
parameters:
url: { type: string, required: true }
returns: { type: object }
---
# Web Research
Search first, then fetch the best results; download only when the user asks for
the file itself.Acceptance #6 — the file_io mixin's real tools.
---
name: file-operations
description: Read, write, and edit files on the local workspace. Use when the user asks to inspect or modify files.
version: 1.0.0
metadata:
gaia:
security_tier: community
permissions:
- filesystem:read
- filesystem:write:./**
tools:
- name: read_file
description: Read the contents of a file
parameters:
file_path: { type: string, required: true }
returns: { type: object }
- name: write_file
description: Write content to a file
parameters:
file_path: { type: string, required: true }
content: { type: string, required: true }
returns: { type: object }
- name: edit_file
description: Replace a span of text in an existing file
parameters:
file_path: { type: string, required: true }
old_content: { type: string, required: true }
new_content: { type: string, required: true }
returns: { type: object }
---
# File Operations
Read before you write; prefer `edit_file` for in-place changes over rewriting a
whole file with `write_file`.A Hermes skill (left) and what gaia skill migrate --from hermes produces
(right) — the modeled metadata.hermes fields become metadata.gaia, and the
result lands experimental. Unmodeled ones (category) stay under
metadata.hermes; experimental being the omitted default, the emitted file
need not restate it (gaia skill info always shows the effective tier).
# BEFORE — Hermes
---
name: pdf-extract
description: Extract structured data from a PDF.
version: 1.0.0
metadata:
hermes:
category: documents
requires:
tools: [read_file]
---
# Extract from PDF
...# AFTER — gaia skill migrate --from hermes
---
name: pdf-extract
description: Extract structured data from a PDF.
version: 1.0.0
metadata:
gaia:
security_tier: experimental
tools_required:
- read_file
---
# Extract from PDF
...An OpenClaw skill and the mapping migrate computes for it.
metadata.openclaw requirements map to metadata.gaia fields and trust resets
to experimental.
# BEFORE — OpenClaw
---
name: git-status
description: Summarize the working tree status.
version: 0.3.0
metadata:
openclaw:
requires:
bins: [git]
---
# Git Status
...# AFTER — gaia skill migrate --from openclaw
---
name: git-status
description: Summarize the working tree status.
version: 0.3.0
metadata:
gaia:
security_tier: experimental
permissions:
- shell:execute:git
---
# Git Status
...Every degraded condition lands on a conservative, well-defined floor — nothing breaks because a GAIA feature is absent.
| Condition | Floor behavior |
|---|---|
Bare standard skill (only name + description) |
Loads as an instruction-only skill: security_tier: experimental, no tools, no permissions. The most restrictive defaults. Matches Agent Skills → compatibility rule. |
No metadata.gaia block |
All GAIA fields take their omitted-defaults (above). A standard runtime ignores metadata.gaia entirely → lossless both directions. |
compatibility / allowed-tools present |
Ignored (not part of GAIA's adopted base). The skill still parses. |
tools_required names a tool unknown to the framework |
Not yet checked. The Phase 1 validator coerces tools_required to strings and stops there — no catalog lookup runs, because the active registry is assembled dynamically (see Open question 1). The field is inert until #1451 consumes it. When the check does land, an unknown name fails loudly; a name that is valid but simply not active in the current agent's registry stays scoping, not a defect — omitted from selection and logged, never silently invented. |
| Skill directory fails to parse | Skipped from discovery and surfaced — logged at ERROR, listed in SkillManager.discovery_errors, printed by gaia skill list, which exits 4 — except under a read-only claude-import root, where it is a warning logged once and the exit stays 0. Never quietly missing. |
Two roots ship the same skill name |
The higher-precedence copy wins and the loser stays visible via SkillManager.shadowed() / gaia skill info. Precedence is auditable, not silent. |
| A skill body references a file outside its own directory | Rejected — SkillManager.resource_path refuses traversal, as does .zip import. |
| Enforcement layers not yet shipped | /api/skills and the sandbox degrade to absent, not permissive: an unenforceable permission refuses the load rather than loading unguarded. Install-time tier ceilings now ship. |
| Tier signal unavailable | Falls to experimental — the most-restrictive tier — never to a more-permissive default. Imported and migrated skills are re-stamped experimental regardless of the tier they claim, and an unsigned or untrusted-key install is collapsed to experimental however high it claims. |
Future-gated phases are additive with precedence. The tools_required
signal that #1451 consumes returns [] cleanly until #887 lands
(tool-loader.mdx),
so a recipe skill authored today is valid but inert until its consumer ships —
it never blocks or errors.
This document revises an earlier draft of skill-format.mdx. The most
important correction:
The base standard is Agent Skills (agentskills.io), not OpenClaw. The prior draft framed "OpenClaw / ClawHub" as the external standard to be compatible with. In fact agentskills.io (as used by Claude Code) is the base GAIA adopts; OpenClaw is just one of several compatible third-party formats, alongside Hermes. This reconciles
skill-format.mdxwith Agent Skills, which had already flagged "OpenClaw/ClawHub" as a placeholder from an earlier draft.
The second structural correction: GAIA-specific fields move from the top level
into metadata.gaia. The prior draft put permissions / security_tier /
requirements / tools at the top level, which collides with the standard's
namespace and breaks lossless cross-runtime parsing. Nesting them under
metadata.gaia mirrors the convergent prior art (Hermes metadata.hermes,
OpenClaw metadata.openclaw).
OpenClawAdapterruntime adapter is dropped from v1. Compatibility is migration-only — viagaia skill migrate, which ships (#692); it is the runtime adapter that is deferred, not the migration path.- The flat top-level schema is superseded.
permissions,security_tier,requirements, andtoolsare now undermetadata.gaia. Top-levelversionis the only GAIA superset key outsidemetadata. compatibilityandallowed-toolsare not part of GAIA's base. The prior reliance onallowed-toolsfor permission scoping is removed; permissions come frommetadata.gaia.permissions.- The five-root discovery list is superseded by three. Project-local
./.gaia/skills/is deferred — see Agent Skills → Discovery. The registry-lock root is not a fourth root:gaia skill installwrites~/.gaia/skills/skill-lock.jsonbeside the skills it installs, so the lock is provenance over the existing user root, not a new discovery location. - Still greenfield, still PROPOSED: the local-capability permission sandbox
and run-time enforcement,
/api/skillsand the UI panel, andgaia skill update. Do not build against these.
No longer a retraction — these now exist. From
#888: gaia.skills, SkillManager,
Agent.load_skill, ~/.gaia/skills/, and
gaia skill list|info|create|import|export. From
#2467:
gaia skill search|install|remove|publish (plus keygen/trust), bundle
signing, install-time tier-ceiling enforcement, and skill-lock.json. From
#692: gaia skill migrate and
gaia.skills.migrate. An earlier revision of this document said the #888 symbols
were "verified absent on main"; that is no longer true of any of these sets.
A format contract is judged by conformance and losslessness, not runtime speed. The bar:
| KPI | Type | Target |
|---|---|---|
| Cross-spec field-name match | Headline (contract) | tools_required (and every other emitted field) is byte-identical across #887, #1451, and this spec — zero drift. |
| Lossless round-trip | Correctness | A bare agentskills.io skill loads in GAIA unchanged; a GAIA skill's standard fields parse in a standard runtime with metadata.gaia ignored, no parse break. |
| Migration fidelity | Correctness | gaia skill migrate output validates against this schema and lands experimental. (Met — verified against 26 real published ClawHub skills.) |
| No hallucinated runtime | Grounding | Every cited symbol resolves on main or is marked PROPOSED. (Met — the Phase 1 symbols now resolve in src/gaia/skills/.) |
| Example validity | Grounding | Every example parses as YAML; each tool skill's tools block round-trips against its tools.py. |
| Acceptance coverage | Completeness | 100% of #691's acceptance criteria map to a section (see Traceability). |
The format (Phase 0) and the runtime core (Phase 1) have shipped; the remaining phases are additive. Each phase holds the prior floor until it lands.
Success criteria:
- Schema decided: adopted base +
metadata.gaia;toolsvstools_requiredseparated;compatibility/allowed-toolsexcluded. - Agent Skills reconciled — mapping table nests
metadata.gaia.*, the placeholder Note becomes a Division-of-authority Note, andallowed-tools/compatibilityare marked excluded. - All example skills parse; the three acceptance-#6 skills use only real registry tool names.
- Traceability covers 100% of #691 acceptance criteria.
Phase 1 — Loader, validator, discovery, CLI core (SHIPPED, #888)
What landed:
gaia.skills.format—SKILL.mdparser, writer, and validator (Skill,SkillTool,SkillRequirements,GaiaMetadata,parse_skill,parse_skill_file,parse_skill_metadata,validate_skill). Round-trip is identity: unknown top-level keys, othermetadata.<vendor>namespaces, and unknownmetadata.gaiakeys are all preserved, so passing a foreign skill through GAIA loses nothing.compatibility/allowed-tools/disallowed-toolsparse and are ignored, exactly as specified above.gaia.skills.manager—SkillManagerover three roots (agent-bundled→user→claude-import), progressive disclosure (discover= frontmatter,load= + body,resource_path= on-demand files, traversal-checked), auditable shadowing (shadowed()), surfaced parse failures (discovery_errors), and optional hot-reload viastart_watching().gaia.skills.loader— importstools.pyand registers each@toolas<skill>/<tool>in_TOOL_REGISTRY, cross-checking the manifest against the real signatures. No partial load: any failure restores the registry byte-for-byte.gaia.skills.permissions— the<domain>:<level>[:scope]grammar, the connector bridge fornetwork/mcp, and the loud refusal of local-capability domains.gaia.skills.sets(#2466) — the declarativeskills:/skill_sets:/default_skill_set:manifest grammar (see Manifest-side grammar), parsed once and shared bygaia.hub.manifestand the baseAgent, plus per-launch selection (SKILL_MANIFEST,select_skill_set,load_skill_set). An undeclared set name raisesSkillSetErrorrather than falling back. The email agent is the reference consumer.Agent.load_skill/unload_skill/SKILL_DIRS/skill_manager/loaded_skills— plusget_skills_system_prompt(), auto-discovered by_get_mixin_prompts(), which returns""when no skill is loaded so every existing agent's prompt stays byte-identical.gaia skill list|info|create|import|export.
Success criteria — met:
- A tool skill loads and its tools register and call under
<skill>/<tool>. - A bare standard skill loads instruction-only at
security_tier: experimental. - Validation fails loudly on a
tools↔tools.pymismatch — missing tool, undeclared extra, parameter-name mismatch, required/optional flip, or type contradiction — with no partial load.
Deliberately deferred out of Phase 1: tier-ceiling enforcement, the
project-local ./.gaia/skills/ root, and network-egress enforcement. (The
install-time half of the tier ceiling has since shipped with the marketplace —
see Phase 4.)
Phase 2 — Permission enforcement + load-time tier ceilings + sandbox (PROPOSED, #1019)
The connector bridge shipped early, in Phase 1: network:* / mcp:connect
already resolve to ConnectorRequirement at load-time. The install-time tier
ceiling shipped with the marketplace (Phase 4). What remains is the sandbox that
makes local-capability domains loadable at all, the load-time ceiling — which
covers a skill that never passed through install, e.g. one from gaia skill create/import or copied into a root by hand — and network-egress policy over
the declared network requirements.
Success criteria:
- A hand-placed skill requesting a permission above its tier ceiling fails to load (installing one already fails).
filesystem/database/desktop/envroute to the sandbox and load instead of being refused (shell:execute:<binary>already loads through the policed binary bridge).- A declared
network:<level>:<scope>actually constrains egress.
Phase 3 — Agent UI dashboard (PROPOSED; #701)
/api/skills mirroring /api/agents, plus the skills management panel.
gaia skill migrate --from {hermes,openclaw,auto} was slated for this phase and
shipped ahead of it (#692) — it needs
no UI surface, so it did not wait for one. Visual import remains part of this
phase: it is the same migrator behind a panel that does not exist yet.
Success criteria:
- The panel lists installed skills with tier + version and supports install/remove (acceptance #4).
- The panel offers OpenClaw import over
gaia.skills.migrate, reusing the shipped mapper rather than a second implementation.
Phase 4 — Marketplace (SHIPPED, #2467) + consumer wiring (PROPOSED; #887 / #1451)
Shipped: the hub's skills lane (POST /publish/skill, the skills/ R2
namespace, GET /index.json filtered by type: "skill"), gaia skill search|install|remove|publish, Ed25519 bundle signing with a trust store,
install-time tier ceilings, skill-lock.json, and the declarative skills:
block. The pre-publish security audit gate is
#2468 — publish calls it and fails
loudly when its engine is absent rather than publishing un-audited.
Still proposed: gaia skill update; #887 emits conformant SKILL.md; #1451 unions
tools_required.
Success criteria:
- A published skill installs by
name@<range>, lock-tracked, and loads into an agent — including a non-bundled one. (Met.) - An unsigned skill cannot install as
verified;experimentalneeds--allow-experimental; the per-tier permission ceiling is honored. (Met.) - #887-synthesized files validate against this schema unchanged.
- #1451 unions a matched skill's
tools_requiredahead of semantic results (the contract intool-loader.mdx).
The five design forks (base standard, metadata.gaia nesting, top-level
version, migration-only v1, excluding compatibility/allowed-tools) are
locked by the maintainer and are recorded as settled above. What remains are
implementation-level questions for whoever builds the runtime:
-
tools_requiredvalidation timing. Fail-vs-skip is not open — GAIA's fail-loudly policy settles it: a name unknown to the framework is a manifest defect and fails loudly; a name valid but inactive in the current agent's registry is scoping (omitted + logged, not an error). The genuine open nuance is when the unknown-name check runs — at install against a static tool catalog, or deferred — since the active registry is assembled dynamically from mixins and other skills, so the full tool universe may not be known at install time. Affects #1451 robustness. -
Skill→skill dependencies. The prior draft allowed a skill to depend on other skills (
requirements.skills). Keep it undermetadata.gaia.requirements, or defer dependency resolution to the marketplace (#647)? -
/api/skillsinstall sources. Registry-only, or also local path / URL? The latter widens the install-time security surface. -
Body-injection scanning.Resolved by #2468: every skill's body is scanned, and the tier decides the consequence. The "required forcommunity+, advisory forexperimental" split was the right instinct, but it does not need to be its own policy — body findings feed the same tier gate as code findings, so the split falls out for free: atexperimentala body finding is surfaced and does not gate, atcommunityit forcesREVIEW, atverifiedamediumis already enough to hold it. See Pre-publish security audit for the scan itself.Two decisions inside it are worth recording, because both trade precision against evasion:
- Fenced and quoted text is downgraded one step, never ignored. A skill that documents injection patterns so the model can refuse them is the good case, so blocking it would punish the careful author. But fenced text still reaches the model — models do not reliably treat it as inert data — so a code block buys a severity step, not a pass. Otherwise "put the directive in a fence" would be the standing bypass.
- Prohibitions are not directives. "NEVER dump the environment" is the security instruction, not a request for one, so a prohibition immediately before a match suppresses it. Rules whose own pattern opens with a negation (concealment's "do not tell the user") are unaffected, because the check looks only at text before the match.
-
Settled with Phase 1: this document stays indocs/spec/relocation.docs/plans/. Agent Skills is the spec surface — it already lives indocs/spec/and satisfies #691's "documented indocs/spec/" acceptance. Moving this doc too would split the field grammar from the architecture across two directories for no reader benefit, and would break every inbound/plans/skill-formatlink, including the ones the runtime's own error messages emit (FORMAT_DOCS_URLinskills/errors.pypoints athttps://amd-gaia.ai/docs/plans/skill-format). Revisit only if the two documents ever merge.
The format is grounded on real primitives, and the Phase 1 runtime now exists. Only the sandbox/run-time-enforcement and REST/UI layers are greenfield; the marketplace shipped in #2467.
| Area | Symbol / fact | file:line |
State |
|---|---|---|---|
| Tool registry | _TOOL_REGISTRY: dict[str, dict] |
agents/base/tools.py:16 |
Exists |
| Tool decorator | @tool(func, *, atomic, display_label, timeout, …) |
agents/base/tools.py:19 |
Exists |
| Mixin map | KNOWN_TOOLS (rag, browser, file_io, …) |
agents/registry.py:38 |
Exists — source for the 3 example skills |
| Agent base | Agent, _compose_system_prompt, _get_system_prompt |
agents/base/agent.py:245, :591, :669 |
Exists |
| Connector grant | ConnectorRequirement(connector_id, scopes, reason) |
connectors/providers/base.py:23 |
Exists — permission bridge |
| Required connectors | REQUIRED_CONNECTORS: ClassVar[...] |
agents/base/agent.py:295 |
Exists |
| Governance | DecisionType = Literal["ALLOW","REVIEW","BLOCK"] |
governance/schemas.py:17 |
Exists — run-time, not tiers |
| Per-agent identity | namespaced_agent_id (builtin:/custom:sha256:/installed:) |
agents/registry.py:392 |
Exists — scoping precedent |
| Default model | DEFAULT_MODEL_NAME = "Gemma-4-E4B-it-GGUF" |
llm/lemonade_client.py:119 |
Exists |
| Model tiers / device | build_model_tiers(), DeviceConfig(ctx_size=…) |
agents/registry.py:335, :227 |
Exists — requirements references these |
| Agents UI router | /api/agents list/detail/export/import |
ui/routers/agents.py:130, :174, :184, :222 |
Exists — /api/skills mirrors it |
| Bundle format | .zip + bundle.json, BUNDLE_FORMAT_VERSION = 1 |
installer/export_import.py:40 |
Exists |
| Builder template | scaffolds agent.py, emits no SKILL.md |
agents/builder/template.py |
Exists |
| Skill parser / validator | Skill, parse_skill, parse_skill_file, parse_skill_metadata, validate_skill |
skills/format.py |
Exists (#888) |
| Discovery + precedence | SkillManager, SkillRoot, ROOT_AGENT_BUNDLED/ROOT_USER/ROOT_CLAUDE_IMPORT, user_skills_dir() |
skills/manager.py |
Exists (#888) — 3 roots |
| Tool registration | register_skill_tools, unregister_skill_tools (<skill>/<tool>, no partial load) |
skills/loader.py |
Exists (#888) |
| Permission grammar + bridge | Permission, parse_permissions, connector_requirements, refuse_unbridged_permissions, CONNECTOR_BRIDGED_DOMAINS, LOCAL_CAPABILITY_DOMAINS |
skills/permissions.py |
Exists (#888) — bridge only |
| Agent integration | load_skill, unload_skill, SKILL_DIRS, skill_manager, loaded_skills, get_skills_system_prompt |
agents/base/agent.py |
Exists (#888) |
| Skill-set grammar | SkillRef, SkillSets, SkillSetResolution, parse_skill_sets |
skills/sets.py |
Exists (#2466) — skills: / skill_sets: / default_skill_set: |
| Manifest wiring | AgentManifest.skill_sets |
hub/manifest.py |
Exists (#2466) — malformed blocks raise ManifestError |
| Skill-set selection | SKILL_MANIFEST, skill_sets, select_skill_set, resolve_skill_set, load_skill_set, active_skill_set |
agents/base/agent.py |
Exists (#2466) — one set per launch |
| CLI | gaia skill list|info|create|import|export |
skills/cli.py |
Exists (#888) |
| Migration | migrate_text, migrate_skill_dir, install_migrated, detect_vendor; gaia skill migrate |
skills/migrate.py |
Exists (#692) — OpenClaw/Hermes → GAIA |
| Trust reset | reset_security_tier() — one path shared by import and migrate |
skills/format.py |
Exists (#692) |
| Errors | SkillError, SkillValidationError, SkillNotFoundError, SkillPermissionError, SkillSetError |
skills/errors.py |
Exists (#888, #2466) |
| Local-capability permission sandbox | — | NOT FOUND | Greenfield — PROPOSED (#1019) |
| Install-time tier ceiling | enforce_tier_ceiling(), effective_tier() |
skills/tiers.py |
Exists (#2467) |
| Bundle signing / trust store | sign_bundle(), verify_bundle(), TrustStore |
skills/signing.py |
Exists (#2467) |
/api/skills router + UI panel |
— | NOT FOUND | Greenfield — PROPOSED |
gaia skill install|remove|search|publish |
_handle_install, _handle_publish, … |
skills/cli.py |
Exists (#2467) |
gaia skill update |
— | NOT FOUND | Greenfield — PROPOSED (marketplace) |
| Hub catalog lane (server side) | skill catalog type, POST /publish/skill, skills/ R2 prefix, SKILL.md publish validation, audit gate |
workers/agent-hub/src/ |
Exists (#2467) |
Project-local ./.gaia/skills/ root |
— | NOT FOUND | Deferred — PROPOSED |
Registry lock (skill-lock.json) |
SkillLock, LockEntry |
skills/lock.py |
Exists (#2467) |
| Lock drift detection + relock | check_drift(), assert_no_fatal_drift(), relock(), gaia skill lock |
skills/drift.py |
Exists (#2467) |
Blocking: none. This is a foundational format contract; its design inputs are external standards plus the in-repo drafts.
Design inputs: Agent Skills (base), Hermes
(metadata.hermes), OpenClaw (metadata.openclaw), Claude Code (reference
impl); in-repo Agent Skills (architecture / compatibility
surface) and tool-loader.mdx Part 3 (the tools_required
contract).
Downstream consumers. The features whose contract this format must satisfy. A closed tracking issue means that design is settled and already references this format — not that the dependency is gone.
| Consumer | What it needs | Tracking issue |
|---|---|---|
| #887 skill auto-synthesis | Emits SKILL.md — the exact frontmatter, esp. tools_required. |
open |
| #553 self-improving agent | Skill extraction emits conformant SKILL.md — same producer contract as #887. |
open |
| #1451 tool-loader Part 3 | Reads tools_required to union into per-turn tool selection. |
open (gated on #887) |
| #648 OEM bundling | A definition of "a skill" to pre-bundle SKU defaults. | open |
| #647 marketplace | The format + the three tiers to publish/promote. | closed (design settled) |
| #462 Agent Manifest | A skill-level complement to gaia-agent.yaml. |
closed (design settled) |
Every acceptance criterion from #691, verbatim, mapped to its section and what it guarantees a consumer.
| #691 acceptance criterion | Section | What it guarantees |
|---|---|---|
☑ SKILL.md spec documented in docs/spec/ |
Agent Skills is the docs/spec/ surface; this doc stays in docs/plans/ as the field grammar — settled, see Open question 5 |
A single authoritative field grammar. |
☑ Permission model defined with granular domain:level syntax |
Permission model | <domain>:<level>:scope grammar + the ConnectorRequirement bridge — no parallel grant ledger. Grammar and bridge implemented; enforcement of local-capability domains deferred (they refuse). |
| ☑ Security tiers defined + enforced with promotion path | Security tiers | Three install-time tiers + experimental→community→verified, distinct from run-time governance. Field implemented; ceilings deferred to Phase 2. |
| ☐ Agent UI configuration dashboard has skills management panel | Agent UI dashboard support | A /api/skills shape mirroring the live /api/agents panel (PROPOSED — Phase 3). The CLI half (install/remove) it would call now ships. |
☑ CLI: gaia skill list, install, remove |
CLI | All three shipped, alongside info/create/import/export/migrate/search/publish/keygen/trust (#2467, #692). |
| ☑ Example skills written for 3 existing agents | Example skills 4a–4c | rag-search, web-research, file-operations — each using only real registry tool names. |