registry.json is a generated artifact. It is not committed to git; the
source of truth is the SKILL.md files under categories/.
python tools/update_registry.pyOn every push to main that touches categories/, tools/, keys/,
manifest-schema.toml or the workflow itself,
publish-registry.yml validates the
corpus, regenerates registry.json, signs it, verifies the signature, and
uploads both files to the rolling GitHub release registry-latest:
https://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry.jsonhttps://github.com/GrayCodeAI/graycode-skills/releases/download/registry-latest/registry-signature.json
This is the index Rho reads for rho skills search, info and trending. There
is no CDN; the GitHub release is the distribution point. Each run also keeps
both files as a 90-day Actions artifact (skill-registry) for forensics.
The registry is signed with Ed25519 only. The public key is committed at
keys/registry-ed25519.pub and pinned by Rho:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAr9I2NG1Sih9Mu04/eOA8FmJhczSLBYiXeLAl1rqusQU=
-----END PUBLIC KEY-----
The private key exists only as the SKILLS_ED25519_PRIVATE_KEY GitHub Actions
secret. If that secret is unset the publish job fails; it never falls back to
another scheme or an unsigned upload.
registry-signature.json is the JSON printed by
python tools/sign_manifest.py sign registry.json --ed25519:
{
"target": "registry.json",
"sha256": "<lowercase hex SHA-256 of the exact registry.json bytes>",
"algorithm": "ed25519",
"signature": "<hex Ed25519 signature>"
}The signed message is the 64 ASCII bytes of the lowercase hex digest, not the raw 32-byte digest.
A client must:
- download both files;
- require
algorithm == "ed25519"; - recompute the SHA-256 of the downloaded
registry.jsonbytes and require it to equalsha256; - verify
signatureover the hex digest with the pinned public key; - refuse to use the index if any step fails (no unsigned fallback).
With this repository checked out:
python tools/sign_manifest.py verify registry.json \
--signature-file registry-signature.jsonverify uses keys/registry-ed25519.pub unless --key or
SKILLS_ED25519_PUBLIC_KEY is given. With OpenSSL 3 only:
jq -r .signature registry-signature.json | xxd -r -p > registry.sig
printf '%s' "$(shasum -a 256 registry.json | cut -c1-64)" > registry.sha256
openssl pkeyutl -verify -pubin -inkey keys/registry-ed25519.pub \
-rawin -in registry.sha256 -sigfile registry.sigThe publish workflow runs the same verify against the committed key before
it uploads anything.
python tools/sign_manifest.py keygen --private-out <secure path> --public-out keys/registry-ed25519.pub- Store the private PEM as the
SKILLS_ED25519_PRIVATE_KEYsecret; never commit it. - Update the expected key in
tests/test_sign_manifest.pyand ship the new pinned key in a Rho release before, or together with, the first registry signed by the new key. Old clients reject registries signed by a key they do not pin.
At about 6 MB, committing registry.json would add diff noise and slow clones.