Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/plans/skill-format.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ 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>}
Expand All @@ -619,7 +620,18 @@ 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 — the same rule agent `dependencies:` use. Installs are recorded in
`~/.gaia/skills/skill-lock.json` with the requested range, the resolved version,
the artifact SHA-256, the signing key, and the tier actually installed at.
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):

Expand Down Expand Up @@ -1473,6 +1485,7 @@ Only the sandbox/run-time-enforcement and REST/UI layers are greenfield; the mar
| 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) |

---

Expand Down
115 changes: 110 additions & 5 deletions docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2022,7 +2022,7 @@ Author and manage agent skills. A skill is a directory whose only required file
gaia skill {list,info,create,import,export,migrate} [OPTIONS] # author locally
gaia skill deltas <name> [OPTIONS] # what an agent LEARNED
gaia skill audit <dir> [OPTIONS] # pre-publish gate
gaia skill {search,install,remove,promote,publish,keygen,trust} [OPTIONS] # marketplace + trust
gaia skill {search,install,remove,lock,promote,publish,keygen,trust} [OPTIONS] # marketplace + trust
```

Skills are discovered from three roots, highest precedence first:
Expand Down Expand Up @@ -2213,7 +2213,21 @@ Skills are discovered from three roots, highest precedence first:
NAME VERSION TIER TOOLS DESCRIPTION
web-research 1.2.0 community 1 Search the web and summarize results
incident-review 0.3.1 experimental 0 Structure a postmortem from an incident log

Catalog: hub, fetched just now
```

The trailing line is the catalog's age. Offline still works — GAIA serves the
cached catalog rather than refusing — but it says how old that copy is, and
warns loudly once it passes a week:

```
⚠ This catalog was last refreshed 93 days ago — over 7 days old. The hub is
unreachable, so skills published since are missing and skills unpublished
since are still listed, including any whose security tier changed.
```

`--json` carries the same facts as `age_seconds`, `age_text`, and `stale`.
</Tab>

<Tab title="install">
Expand All @@ -2239,6 +2253,73 @@ Skills are discovered from three roots, highest precedence first:
gate passes.
</Tab>

<Tab title="lock">
```bash
gaia skill lock [--check | --relock] [--json]
```

| Option | Description |
|--------|-------------|
| `--check` | Report drift, change nothing (the default) |
| `--relock` | Re-record the current state as intended |
| `--json` | Emit JSON instead of text |

Compares every skill in `~/.gaia/skills` against `skill-lock.json` — version,
the tier install enforced, and a digest of the files — and reports skills the
lock has lost track of plus skills it tracks that are gone. Exits `4` when
anything differs, `0` when the lock and the disk agree.

```
⚠ 2 difference(s) between ~/.gaia/skills and skill-lock.json (1 blocking, 1 advisory):

web-research
✗ [content] The files in ~/.gaia/skills/web-research no longer hash to the
digest recorded at install — something was added, removed, or edited.
→ Reinstall the attested copy with 'gaia skill install web-research --force'.
my-notes
⚠ [untracked] 'my-notes' is installed but skill-lock.json does not track it.
→ Expected if you created, imported, or migrated it.
```

Untracked skills are normal right after `create` / `import` / `migrate`;
`--relock` starts tracking them as `source: local`.

<Note>
**After upgrading from a build that predates content digests**, skills
installed by the older build have nothing to compare against. They report as
*unverifiable*, not as changed — `gaia skill list` says so in those words and
still exits `0`:

```
⚠ 2 skill(s) in ~/.gaia/skills predate content digests, so skill-lock.json
cannot verify them:
```

`gaia skill lock` still exits `4` here: it is an explicit verification verb,
and "could not verify" is not "verified clean". Don't gate CI on it without
running `--relock` first. Locally-authored and `experimental` skills are
recorded by that one command; a `community` / `verified` one needs
`gaia skill install <name> --force` instead (see the warning below).

A **corrupt or future-schema** `skill-lock.json` is different: it is reported
alongside the `gaia skill list` inventory (exit `4`), and every skill in
`~/.gaia/skills` refuses to load until the file is fixed or deleted —
including purely local ones. Nothing can tell an attested skill from a local
one while the lock is unreadable, so loading anyway would let corrupting one
file switch the check off.
</Note>

<Warning>
`--relock` **refuses** to re-record a `community` or `verified` skill whose
files changed, **or one that has no recorded digest at all**. Writing a digest
in either case would leave the lock asserting a publisher signature over bytes
that signature never covered — and the no-digest case is where every upgrade
from a pre-digest build starts, so it is the likeliest way to launder an
attestation, not the rarest. Reinstall it, or `gaia skill remove` then
`gaia skill import` to keep the edit at `experimental`.
</Warning>
</Tab>

<Tab title="promote">
```bash
gaia skill promote <name>
Expand Down Expand Up @@ -2340,8 +2421,8 @@ stamp is a provenance claim, not a runtime sandbox.

Hub installs are tracked in `~/.gaia/skills/skill-lock.json`. Discovery answers
*what is on disk*; the lock answers what was **requested**, what **resolved**, what
the artifact **hashed** to, who **signed** it, and which tier it was installed at
after the ceiling was applied:
the artifact and the installed files **hashed** to, who **signed** it, and which
tier it was installed at after the ceiling was applied:

```json
{
Expand All @@ -2356,6 +2437,7 @@ after the ceiling was applied:
"hub_url": "https://hub.amd-gaia.ai",
"artifact_sha256": "9f2c…",
"artifact_filename": "web-research-1.2.0.zip",
"content_digest": "sha256:4b1d…",
"claimed_tier": "community",
"attested_tier": "community",
"installed_tier": "community",
Expand All @@ -2373,8 +2455,25 @@ after the ceiling was applied:
}
```

Only hub installs are tracked — a skill from `create` or `import` has no hub
provenance to record.
`gaia skill install` writes `source: "hub"` entries; a skill from `create` or
`import` has no hub provenance, so install never invents one. `gaia skill lock
--relock` may record it as `source: "local"`, which claims exactly the origin it
has.

`content_digest` covers the **installed** directory (after the tier re-stamp), not
the downloaded archive. That is what makes post-install tampering detectable:
`gaia skill lock` compares against it, and a `community` / `verified` skill whose
files no longer match **refuses to load** — those bytes are not the ones the
signature covered, so the tier badge no longer means anything. An `experimental`
or locally-authored skill warns and keeps working; nothing attested it, and
editing a skill you wrote is the normal workflow.

<Note>
`skill-lock.json` has no integrity protection of its own — anyone who can write
to it can rewrite a digest. It defends against tampering with the *skill
directory* (a swapped bundle, an edited manifest, a downgraded version), which
until now was entirely silent.
</Note>

**Examples:**

Expand Down Expand Up @@ -2415,6 +2514,12 @@ gaia skill install some-skill --allow-experimental
gaia skill remove web-research
```

```bash Verify nothing changed since install
gaia skill lock # exits 4 if anything drifted
gaia skill lock --json | jq '.drifts[] | select(.fatal)'
gaia skill lock --relock # accept your own edits
```

```bash Publish your own
gaia skill keygen # once, per machine
export GAIA_HUB_TOKEN=...
Expand Down
11 changes: 9 additions & 2 deletions docs/spec/agent-skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,12 @@ The user root is **also** the registry-lock root: `gaia skill install` writes
`~/.gaia/skills/skill-lock.json` beside the skills it installs
([#2467](https://github.com/amd/gaia/issues/2467)). The lock is provenance, not a
fourth discovery root — discovery still reads the directory, and a skill created
or imported locally simply has no lock entry.
or imported locally has no lock entry until `gaia skill lock --relock` records one
as `source: local`. Loading a skill from the user root does check it against the
lock: a `community` / `verified` install whose files changed since is refused,
because those are not the bytes its signature covered. If the lock itself is
unreadable, **every** user-root skill is refused — including local ones — since
nothing can tell an attested skill from a local one at that point.
</Note>

### Progressive disclosure
Expand Down Expand Up @@ -691,7 +696,9 @@ immutability rules ([#2467](https://github.com/amd/gaia/issues/2467)).
resolves a SemVer range against the catalog, verifies the artifact checksum and
the bundle signature, enforces the tier ceiling, and records the outcome in
`~/.gaia/skills/skill-lock.json` ([#2467](https://github.com/amd/gaia/issues/2467),
[#647](https://github.com/amd/gaia/issues/647)); publish the other direction with
[#647](https://github.com/amd/gaia/issues/647)) — including a digest of the
installed files, which `gaia skill lock` compares against later; publish the
other direction with
`gaia skill publish`. Off-hub, `gaia skill import <folder|zip|url>` copies a
skill in (always stamped `experimental` — no hub provenance to trust) and
`gaia skill export <name>` produces the bundle. See
Expand Down
8 changes: 7 additions & 1 deletion src/gaia/agents/base/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2161,11 +2161,17 @@ def load_skill(
Raises:
SkillNotFoundError: no discovery root contains that skill.
SkillValidationError: the manifest is invalid or contradicts
``tools.py``. Nothing is registered.
``tools.py``, or ``~/.gaia/skills/skill-lock.json`` is unreadable
— an unreadable lock cannot tell an attested skill from a local
one, so no user-root skill loads until it is fixed or deleted.
Nothing is registered.
SkillPermissionError: the skill declares a local-capability
permission (``filesystem``/``shell``/``database``/``desktop``/
``env``), which this phase refuses rather than loading
unenforced.
SkillDriftError: the skill was installed from the hub at
``community`` / ``verified`` and its files no longer match
``skill-lock.json`` — the signature covered different bytes.

Example:
class WebAgent(Agent):
Expand Down
Loading
Loading