Skip to content

keys.yml — the keys a realm needs

A realm that calls a vendor with an API key declares that key here: what to call it, where somebody gets one, the value(s) it takes, and how the host can tell whether a value works. Hosts use the declaration to ask for the key by name, show it on the realm’s settings, check a value before storing it, and re-check a stored value while it is in use.

Key entries belong to conventional realms. A captured realm declares credentials the owner binds instead, which is the recommended path for a new realm. Keys and declared credentials compares the two.

keys.yml
- name: brave
displayName: Brave Search
description: Web and news search.
getKeyUrl: https://brave.com/search/api/
fields:
- variable: BRAVE_API_KEY
displayName: API key
validate:
api: brave # an API this realm declares in apis/
operation: webSearch
args: { q: test, count: 1 }
refusedOn: [401, 403, 422] # Brave answers a bad token 422
- name: maps
displayName: Google Maps
fields:
- variable: GOOGLE_MAPS_API_KEY
displayName: API key
- variable: GOOGLE_MAPS_PROJECT_ID
displayName: Project ID
secret: false
validate:
api: maps
operation: geocode
args: { address: "1 Main St" }
interpret: checkMapsAnswer # optional Realm Function, see below
- youtube # shorthand: one field, YOUTUBE, called "youtube"
Field Required Description
name yes Stable id, unique within the realm.
displayName no What a person is shown. Defaults to name.
description no One line on what the key unlocks.
getKeyUrl no Where somebody obtains a key.
fields no The values the key takes. Defaults to one field whose variable is name upper-snake-cased (brave-search → BRAVE_SEARCH).
fields[].variable yes The credential name the value is stored and resolved under — the same name token-env and ${VAR} in apis/ refer to. A captured realm names no variable; see keys and declared credentials.
fields[].displayName no Defaults to the entry’s displayName.
fields[].secret no Default true. false marks a value safe to show back, like a project id.
validate no How to check the values. Without it a key can be set but never checked.

One declaration, any number of APIs. A field’s variable is the credential slot every apis/ entry naming that variable resolves. Two APIs that use one key share one entry, and a person is asked once.

validate names one of the realm’s own API operations and the arguments to call it with. To check a candidate value, the host calls that operation with the value bound into the entry’s credential slot, using the API’s declared auth. The call is the host’s, made through the gateway like any other; the realm never sees the value.

validate field Required Description
api yes An API this realm declares in apis/.
operation yes An operation of that API. Pick the cheapest one any valid key may call.
args no Arguments for the call. Fixed, so the answer can only be about the key.
refusedOn no Response statuses that mean the key is refused. Default [401, 403].
interpret no A Realm Function that decides the verdict from the response, for a vendor whose refusal is not a status.

A check has four outcomes, and every host reports them the same way:

Outcome Meaning
accepted The issuer answered and the key works.
refused The issuer answered and said no.
unreachable No answer: DNS, connection, timeout. Says nothing about the key.
failed An answer that is neither yes nor no, such as a 5xx or a rate limit.

Only refused is a verdict on the key. A host stores nothing it was refused, and may tell the person when a key already stored turns refused. unreachable and failed never block a write and never mark a key bad: a vendor being down is not the key’s fault.

interpret never receives the key. It is called with the check’s response, not the request:

// src/api/keys.ts — Google answers 200 with status REQUEST_DENIED for a bad key
export async function checkMapsAnswer(input: { status: number; body: unknown }): Promise<{ outcome: string; detail?: string }> {
const answer = (input.body as { status?: string }).status
if (answer === 'REQUEST_DENIED') return { outcome: 'refused' }
if (input.status === 200) return { outcome: 'accepted' }
return { outcome: 'failed', detail: `HTTP ${input.status}` }
}

It must return one of the four outcomes. The host applies a short deadline to the whole check; an interpret that throws or overruns is failed. detail is shown to the person and must not repeat anything from the response that could identify the key.

  • Values are never exposed. A host never logs a key’s value, returns it from any read, shows it back unless the field is secret: false, or passes it to realm code.
  • A check may run at any time, repeatedly, and must not change anything at the vendor. validate names a read.
  • Declared keys are the typed credential slot the trust tiers require: a marketplace realm declares its keys here rather than relying on token-env resolving from the environment. This applies to a conventional realm. A captured realm’s typed slot is a declared credential the owner binds, and a captured realm never resolves a secret from the environment.

keys.yml is optional, and a realm without one behaves as before. A host still derives one entry per credential variable the realm’s APIs refer to (token-env, ${VAR} in headers), and presents it as:

Derived from the variable
name the variable, as-is (YOUTUBE_API_KEY)
displayName the variable less a trailing _API_KEY, _KEY or _TOKEN, title-cased (Youtube)
fields one field, the variable
validate none — a derived key can be set, never checked

A declared entry takes precedence over a derived one for the same variable, so a realm can adopt keys.yml one key at a time.

A derived entry resolves the way token-env does, so the process environment fallback applies only in the local or first-party tier described under Auth.

Load problems. A validate.api the realm does not declare, a validate.operation that API does not have, an interpret naming no Realm Function, or two entries claiming one variable, is a recorded problem. The entry still loads, without validate.

A realm asks for a secret in one of two ways, and both keep working. A key entry in keys.yml names the variable a value is stored under. A declared credential in credentials.yml names a purpose, and the owner binds a wallet item to it. Declared credentials are the recommended path for new realms.

Key entry (keys.yml) Declared credential (credentials.yml)
Applies to Conventional realms: one installed from a directory, a first-party or org-reviewed realm, or a local one. Captured realms, which run from a copy the owner admitted. See hosted execution.
The realm names The variable, which token-env and ${VAR} in apis/ also name. A purpose: kind, provider, description, docs link. Never a variable or where the secret lives.
The value comes from A person sets the key, and the host stores it under the variable. The owner binds a pasted secret or an existing wallet item when approving the realm.
The host reads it from The world’s credential store, then the process environment in the local or first-party tier (Auth). The owner’s wallet only.
Checking a value validate, with an optional interpret. No check. Binding an existing item checks only that its stored type fits the credential’s kind.
Changing the value Replace it, and the host re-checks it. An approved API operation is pinned to the value it was approved with; a different value needs approval again.
Values One or more fields, each secret or not. One secret per credential.

token-env is deprecated in favour of either declaration: a key entry for the variable in a conventional realm, or a declared credential referenced with credential: in a captured realm. Hosts still read it. A captured API entry that still uses token-env gets an implicit credential whose id is the variable, the same name a derived key entry for that variable takes.

When both name one secret. A key entry and a captured realm reach the same wallet item when a captured API entry still reads it through token-env, or when the owner binds a declared credential to an existing item a key entry stored. Both then use one value. Replacing or deleting that key through the key entry stops every captured API operation approved with the old value, until the owner approves the new one or the approved value is restored. Hosts do not yet warn before such a replacement, so expect to re-approve a captured realm’s operations after changing a key it shares.

The file is a list with one entry per credential, each naming a purpose: an id, a kind, a plain description written for the person who will be asked to bind it, and where somebody gets one. An apis/ entry or a channel then names a credential by that id. connecting() in embabel-ts emits the file; it is equally hand-authored. A realm that declares nothing here behaves exactly as described under keys.yml.

credentials.yml
- description: A bot token for the server you want the assistant in.
docs: https://discord.com/developers/applications
id: bot
kind: bearer
provider: discord
scheme: Bot
- description: The application's public key, used to verify interaction webhooks.
docs: https://discord.com/developers/applications
id: signing
kind: public-key
provider: discord
apis/apis.yml
- auth: bearer
credential: bot
name: discord
operation-ids:
- getGatewayBot
type: openapi
url: discord.json
write-operation-ids:
- createMessage
Field Required Description
id yes What an apis/ entry or a channel names. Unique within the file. Either a lowercase letter followed by lowercase letters, digits and dashes (up to 64 characters), or an environment-variable style name: a letter or underscore followed by letters, digits and underscores (up to 128). The second form is what a credential migrated from token-env keeps.
kind yes One of api-key, basic, bearer, oauth2, public-key.
description yes What the credential is for, written for the person who is asked to bind it. Up to 512 characters.
provider no The service that issues it, when several credentials share one. Letters, digits, _, . and -, starting with a letter or digit, up to 64 characters.
docs no Where to get one. Must start with https://, contain no whitespace, and be at most 512 characters.
scheme bearer only The word in front of the token in the Authorization header, when it is not Bearer (Discord sends Bot <token>). Printable ASCII without whitespace, at most 32 characters. Refused on any other kind.
scopes oauth2 only The scopes the grant asks for. At least one, no repeats, at most 32 entries of at most 128 characters each. Required on oauth2, because an unscoped grant cannot be judged; refused on any other kind.

The file holds at most 32 entries. A host reads it whole and refuses the whole file, loading no credential from it, when:

  • it is not a non-empty list, or an id repeats;
  • an entry has a field not in the table, a missing id, kind or description, or a kind outside the five above;
  • an id, provider, docs, scheme or scopes breaks its rule in the table;
  • an entry is not named by any apis/ entry or channel in the realm, because asking for a secret nothing uses is a reason to doubt the rest of the list.

Referencing one. An apis/ entry names a credential with credential: <id>, a channel names one the same way, and a channel’s webhook signature names one in its own credential field. A reference to an id the file does not declare refuses the entry. One credential can back several entries and channels. An entry carries credential or the older token-env: both together are accepted only when they name the same thing, which is how a realm migrates one key at a time, while two different names, or neither, refuse the entry.

What the owner does. Nothing is bound at install. For each declared credential the owner either picks a key already in their wallet or pastes a new one, which the host files in the wallet for them, and can unbind it again. A binding belongs to one installation of the realm: two installations of the same realm hold their own, and can use different keys. Every change names the installation revision the owner last read, and a stale one is refused. A credential with no binding has its calls refused, and a channel that needs it is blocked until the owner binds it. A host never shows a bound secret back, logs it, or records it in the grant.

A channel in a captured realm references a credential exactly as an apis/ entry does. The channel files themselves are described under channels/, which does not yet cover the captured form.

Validation refuses a keys.yml field it would have to drop. A field with no variable is an error at its path rather than a key that silently does not exist; a misspelled key is a warning naming the likely intended one; a validate naming an API or operation the realm does not declare is a warning.