What’s in the marketplace
registry.feral.sh hosts eight categories of content that plug into any FERAL brain. Install any of them withferal install <name>.
Naming an item
Everything that installs an item names it the same way: by its name, the string in thename column of the catalog and in a skill manifest’s
skill_id.
id, the UUID the registry minted when it was
published. Both work everywhere a reference is accepted, and an id is a
permalink that a later item cannot take over. But nothing you write by
hand should contain a UUID: an AppManifest declaring
skill_dependencies: ["robot_ext"] is the readable, maintainable form,
and it is the supported one.
When a bare name is not enough
Names are unique per(kind, version), not globally, so one name can
match several rows. The registry answers each case with a rule rather
than a guess:
The brain always sends
?kind= when it resolves an app’s skill
dependency, because the kind is known at that call site, so a
cross-kind clash never reaches the user there.
Install flow
GET https://registry.feral.sh/api/v1/item/<name>(or<id>, or either with?kind=/?version=) returns the manifest, signed-tarball URL, SHA-256, Ed25519 signature, and the publisher’s public key.- The client recomputes SHA-256 locally and verifies the detached signature against the publisher’s public key.
- On success, the tarball extracts to the right
~/.feral/<kind>/<id>/directory and pings the running Brain’s hot-reload endpoint (/api/skills/reload,/api/channels/reload,/api/providers/reload, …). - The new capability is live before your next turn.
kind=skill, step 3 reports its own outcome. feral install prints
“Ready to use.” only when the brain confirmed the hot-reload; when the
reload was refused, or no brain was running to answer it, it prints the
brain’s reason and closes with “Installed …, but it is not loaded in the
running brain. Restart the brain to use it.” The files are on disk either
way. The other kinds still ping their reload endpoint best-effort and
cannot report the outcome, because those endpoints do not return one.
Installing from the web client
The Marketplace page runs the same verification, split into two calls so a person can decide with the facts in front of them:POST /api/marketplace/preview {kind, id}performs steps 1 and 2 above, unpacks the verified bundle to a staging directory, and returns the permission list read out of that bundle, the signature status, and a single-useinstall_token. Nothing is written to~/.feral.- The page shows what the item can reach and whether it is verified. On
confirm,
POST /api/marketplace/install {kind, id, install_token}spends the token and installs the staged bytes.
Installing a GenUI app
Apps are not skills and do not use the marketplace install route. An app is a bundle of server-driven surfaces rendered in a sandboxed iframe, so its own risk is data reach, not code. But anAppManifest may declare
skill_dependencies, and skills run Python inside the brain, so an app
install is also a code install and asks accordingly.
POST /api/apps/preview {registry_id}(or{path}/{git_url}) verifies the bundle, keeps the verified tree staged, and returns four things: the app’s identity and signature status, the app’s own reach (itspermissions.networkallowlist, rendered as plain sentences), the skills it will pull in, and a single-useinstall_token.- The skills are split into three lists, because they are three
different decisions:
- already installed — nothing new runs; shown so the app’s total reach is visible,
- will be installed — new code, each one verified against its publisher’s signature and listed with its own permissions,
- cannot be installed — named, with the reason, the actions that will not work without it, and what to do about it.
POST /api/apps/install {install_token}spends the token. It installs the staged bytes and installs each new skill over the verified registry path. Without a token it is refused with 403.
GET /api/apps recomputes missing_skill_dependencies
on every call, so it disappears by itself once you install the skill.
skill_id in skill_dependencies is resolved against the registry
by name, scoped to kind=skill. It is the same string a user would
type, and it is the same string the catalog lists.
A dependency that verified at preview time and then failed to install is
treated differently: the app is rolled back, because a half-working app
that claims to be installed is its own lie.
Publish flow
manifest.json at the root. The required
fields for each kind are listed in
feral-registry/feral_registry/schemas.py
— the registry rejects a publish that’s missing required keys with a
clear error.
Your source directory is the bundle. Put manifest.json at its root
with impl.py beside it, and feral publish tars the directory as it
stands (minus anything in .feralignore), signs it, and uploads it.
Two manifests, and they are not the same document
A publish carries two things, and confusing them is the mistake this repo made at both ends:- the metadata manifest (
manifest_json, the form field):kind,name,version, description, author, plus the per-kind identifier the registry indexes on. This is what the catalog andGET /item/{ref}serve. It is not covered by the bundle signature. - the bundle’s own
manifest.json: the document FERAL loads at install. Forkind=skillthat is aSkillManifest, and it is where the install dialog reads the permission list from, precisely because it is inside the signed tarball.
feral publish derives the first from it, in
cli/publish.py::registry_envelope, and carries your whole manifest
along under original so the marketplace can describe your item without
downloading it. You never write the envelope by hand.
name is your item’s identifier, not its display name
The registry’s key is (kind, name, version), and name is the string
that GET /api/v1/item/{ref} resolves, that feral install <name>
takes, and that an app writes in skill_dependencies. It comes from
your manifest’s identifier field:
brand.name (“Smart Bulb”) and a daemon manifest’s name (“Wristband
Bridge”) are human display names and are deliberately not the key.
Keying on them would make feral install "Smart Bulb" the interface.
version is read off the validated model, not the raw file, so the
version the catalog lists is the version the installed manifest reports.
What publish refuses, and where
The registry opens the tarball and refuses a skill bundle that FERAL could not load: nomanifest.json, unparseable JSON, or a manifest
missing skill_id, brand.name, or description. brand is required
and stays required: it is the name shown in the install dialog. A
validator that accepts at publish and rejects at install is its own
defect, and it is the publisher, not the user, who should see the error.
feral publish refuses the same things locally first, before it spends
an upload on them.
Publishing is a submission, not a release
The registry is acceptance-gated. A successful publish lands your item asstatus=submitted / visibility=private, and it is not
installable until a reviewer approves it: the catalog does not list
it, GET /api/v1/item/<name> answers 404, and the blob route refuses.
feral publish prints the status it got back rather than “Published!”,
because testing an install against your own fresh submission and getting
a 404 is a confusing way to learn this.
Check where yours stands with the publisher submissions endpoint, using
the same bearer token feral publisher login stored:
Trust model
- Every bundle is Ed25519-signed with the publisher’s private key. The signature is stored alongside the blob so clients can re-verify on every install.
- Publishers verify with GitHub OAuth. Your registry handle is your GitHub login. Your public key is registered once and reused for all future publishes.
verifiedbadge is earned by being in theFEATURED_PUBLISHERSallowlist maintained by the FERAL team. Everything else is community.flagendpoint lets anyone report a malicious package. Flags surface in moderation; repeat offenders lose publisher status.
Self-hosting the registry
The whole service is open source at feral-registry/. To run your own:FERAL_REGISTRY_URL=https://my-registry.example.com.