Skip to main content

What’s in the marketplace

registry.feral.sh hosts eight categories of content that plug into any FERAL brain. Install any of them with feral install <name>.

Naming an item

Everything that installs an item names it the same way: by its name, the string in the name column of the catalog and in a skill manifest’s skill_id.
An item also has an 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

Under the hood:
  1. 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.
  2. The client recomputes SHA-256 locally and verifies the detached signature against the publisher’s public key.
  3. 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, …).
  4. The new capability is live before your next turn.
For 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:
  1. 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-use install_token. Nothing is written to ~/.feral.
  2. 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.
An install without a valid token is refused (403), and a bundle that fails verification never gets one, so the browser cannot reach an unverified install path at all.

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 an AppManifest may declare skill_dependencies, and skills run Python inside the brain, so an app install is also a code install and asks accordingly.
  1. 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 (its permissions.network allowlist, rendered as plain sentences), the skills it will pull in, and a single-use install_token.
  2. 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.
  3. 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.
An app whose dependency cannot be verified still installs, without that skill. Refusing outright would leave you with nothing and no next step; instead the shortfall is disclosed before you decide and stays visible on the Apps page — GET /api/apps recomputes missing_skill_dependencies on every call, so it disappears by itself once you install the skill.
The 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

Bundles are tarballs with a 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 and GET /item/{ref} serve. It is not covered by the bundle signature.
  • the bundle’s own manifest.json: the document FERAL loads at install. For kind=skill that is a SkillManifest, and it is where the install dialog reads the permission list from, precisely because it is inside the signed tarball.
You write the second one. 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: no manifest.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 as status=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.
  • verified badge is earned by being in the FEATURED_PUBLISHERS allowlist maintained by the FERAL team. Everything else is community.
  • flag endpoint 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:
Point your clients at it with FERAL_REGISTRY_URL=https://my-registry.example.com.