Skip to main content
GenUI app manifests describe a publisher’s surfaces, capabilities, and permission surface. Because the FERAL host hands those manifests sensitive trust (network grants, screen real estate, postMessage routes), the host refuses to install one whose origin it cannot cryptographically prove. This page is the publisher quickstart. It covers:
  1. Generating a publisher keypair.
  2. Signing a manifest with feral app sign.
  3. Installing it via the API or CLI (--allow-unsigned for local dev).
  4. Verifying a signed manifest from the command line.

Trust model in one paragraph

Every install path goes through feral_core.agents.app_registry.install_app. That method requires a manifest.signed.json envelope sitting next to manifest.json. The envelope is an Ed25519 signature over the canonical JSON of the manifest, plus the publisher’s public key and a key id that the host pins to a row in the publisher_keys namespace of the local vault. If the manifest is unsigned, tampered, or signed by a key the host doesn’t trust, installation raises UnverifiedManifestError (HTTP 422 from POST /api/apps/preview, which is where the check now runs; the install call spends a token the preview minted and cannot be reached without one). The only escape hatch is the explicit allow_unsigned=True flag (CLI --allow-unsigned, API unsigned: true). Choosing it writes an audit_log entry tagged unsigned_install so the supervisor can trace which device admitted which untrusted bundle.

1. Generate a keypair

The host uses BlindVault.put_namespace("publisher_keys", key_id, public_key_b64) internally; admins can also add keys through the API once that ships.

2. Sign your manifest

There is no --private-key flag: signing uses your publisher key at ~/.feral/publisher.key, created on demand, which is the same keypair the registry authenticates publishes with. One keypair per publisher, not one per signing surface. The CLI calls manifest_signing.sign(manifest_dict, private_key, key_id=...) under the hood and emits a SignedManifest envelope:
The host re-derives the canonical JSON during verification, so any field reorder, whitespace change, or value mutation breaks the signature.

3. Install

Installing is two calls. An app may declare skill_dependencies, and a skill runs Python inside the brain, so the install has to say what code it brings before it brings it. See Marketplace → Installing a GenUI app.

Production / signed

The preview runs AppRegistry._verify_source through inspect_app: it finds manifest.signed.json, calls verify, and applies the permissions policy. install_app runs the same method, so the two cannot disagree. On failure you get a 422 with an error envelope of:
An install with no install_token is refused with 403 preview_required, whatever the source. The token is single-use, bound to the staged bytes and to the source that produced them, and expires after five minutes.

Local dev / unsigned (escape hatch)

unsigned: true goes on the preview, because that is where verification happens. The token it mints carries the decision forward.
This still writes unsigned_install to the audit log when the install that follows is performed.

4. Verify a manifest

CI pipelines should run feral app verify immediately after feral app sign so that a fat-fingered key id never escapes the build.

High-trust permissions

If your manifest requests permissions.network = ["*"], install will refuse it unless all of the following hold:
  • The manifest is signed and verified (i.e. allow_unsigned=False).
  • The manifest carries a non-empty permissions.justification string.
  • The installer passes user_high_trust=True (CLI flag / API field).
This is enforced in feral_core.genui.permissions_policy.enforce_install_policy.