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.
- 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.
Validation
Section titled “Validation”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 keyexport 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.
Guarantees
Section titled “Guarantees”- 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.
validatenames 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-envresolving 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.
Realms that declare nothing
Section titled “Realms that declare nothing”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.
Keys and declared credentials
Section titled “Keys and declared credentials”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.
credentials.yml: declaring a credential
Section titled “credentials.yml: declaring a credential”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.
- 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- 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,kindordescription, or akindoutside the five above; - an
id,provider,docs,schemeorscopesbreaks 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.

