apis/
API entries — each .yml file in apis/ is a list of API definitions loaded on world init. Each entry compiles into a typed gateway.<name>.* namespace inside execute_javascript / execute_python.
Trust tiers are normative. In a marketplace/untrusted realm, file://, process-environment
fallback, ${VAR}/raw-header credential interpolation, query/path/body credentials, and directly
networked credential-bearing MCP are rejected at load. Marketplace network access must use a
host-vetted typed credential slot and structured auth profile through the gateway; if the provider
cannot meet the deployment’s marketplace credential policy, that integration is unavailable in the
marketplace tier. The token-env, custom-header interpolation, and credential-bearing MCP forms
documented below are compatibility features for local or explicitly first-party/org-reviewed
installations. Their trust tier is adoption-visible and they never become marketplace-safe merely
because they run in Docker.
The typed credential slot is a key entry in a conventional realm and a declared credential the owner binds in a captured realm. See keys and declared credentials for which applies where.
- url: https://petstore3.swagger.io/api/v3/openapi.json name: petstore type: openapi auth: api-key token-env: PETSTORE_API_KEYEntry fields
Section titled “Entry fields”| Field | Required | Notes |
|---|---|---|
url |
yes | Spec source. HTTP(S), file://, or a bare relative path resolved against the apis.yml file’s parent directory (used for vendored specs — see below). |
name |
recommended | Gateway namespace — gateway.<name>.*. Falls back to a slugified spec title if omitted. Always set this in published realms so the prompt examples work regardless of the spec’s info.title. |
type |
no | openapi (default) or graphql. |
auth |
no | none (default), bearer, api-key, oauth2. See Auth below. |
token-env |
with bearer / api-key | Env-var or credential-store key holding the token. Deprecated in favour of a key entry for the same variable, or of a declared credential in a captured realm. |
headers |
no | Custom HTTP headers; values support ${VAR} interpolation from credential store / env. |
oauth2 |
with auth: oauth2 |
OAuth2 config — see OAuth2 below. |
tags |
no | Allowlist of OpenAPI tag names. Filters huge specs to a coarse subset. |
operation-ids |
no | Exact operationId allowlist. Composes with tags (tags pre-filter, operation-ids picks exact ops). Match is case-insensitive and treats -// as _, so repos/get, repos-get, repos_get all match. |
capability-tags |
no | Capability DECLARATION — what the API is FOR, in the host’s vocabulary (web-search, web-fetch). Lets host code pick a tool by capability instead of by provider name. Unrelated to tags; see Capability tags below. |
Capability tags
Section titled “Capability tags”capability-tags says what an API IS FOR, in a vocabulary the host
defines — web-search (discovers information on the open web from a
query), web-fetch (reads a URL it is given, and cannot discover one).
Most tools are chosen by an LLM reading their descriptions, but some
consumers have to choose without one in the loop: the host’s person
research needs a search tool before any model is asked anything. A
declaration is how such a consumer finds yours.
- url: https://api.example.com/search/openapi.json name: example-search capability-tags: [web-search]Not tags. tags filters WHICH OPERATIONS of the spec become tools,
using the spec’s own OpenAPI tag names. tags: [web-search] therefore
filters the spec down to operations the provider happened to tag
web-search — usually none, leaving the entry with no tools at all. The
two fields do unrelated jobs and may both be set.
The host owns the vocabulary; realms supply the providers. A tag the
host does not recognise is carried but matches nothing, so inventing one
declares nothing. Only web-search and web-fetch are defined today.
Declare on every provider you ship, not just one. Selection is
all-or-nothing per capability: once anything in the world declares
web-search, only declarations count, and an untagged provider in the
same realm stops being selected. Where nothing at all declares the
capability the host falls back to matching tool names and logs that it
did — so a realm that predates this field keeps working, but a realm
that tags half its providers loses the other half.
The tag sits on the ENTRY, so it covers every operation that entry
exposes. Where only part of a spec carries the capability, narrow the
entry with tags / operation-ids — or split it into two entries, each
with its own declaration.
Vendored specs
Section titled “Vendored specs”url accepts a bare relative path. The loader resolves it against the file’s own parent directory, so realms can ship a hand-curated spec next to their apis.yml:
realm-hubspot/└── apis/ ├── apis.yml # url: hubspot-crm.json └── hubspot-crm.json # the spec, vendored in-realmUse this when the upstream provider has stopped publishing OpenAPI specs (or never did), or when you want to pin a specific subset of operations and types without depending on a moving public URL. HTTP(S) and file:// URLs work too — the relative-path mode is just the most ergonomic for vendored specs.
A realm’s producers call the realm’s own APIs. A producer’s operation: names an operation by
its bare id, and another installed realm may ship an operation of the same id — two realms each
vendoring Wikipedia’s page summary both expose pageSummary. A producer declared by a realm resolves
to the operation that realm’s own apis/ (or handlers) supply, wherever the other realm’s sits, so
each realm’s fetches go out through its own entry, with its own headers and credentials. Give the
API entry a name of the realm’s own (bible_wikipedia_summary): API names are unique in a world,
operation ids need not be. Only a producer whose realm supplies no such operation — or a
world-authored producer, which belongs to no realm — can land on another realm’s copy; the host
warns about that once, and a conversation bound to both realms refuses that fetch, naming both
candidates.
Four auth modes plus the implicit headers-only path:
# 1. No auth- url: https://api.example.com/openapi.yaml auth: none
# 2. Bearer token (most REST APIs)- url: https://api.github.com/openapi.json name: gh auth: bearer token-env: GITHUB_PERSONAL_ACCESS_TOKEN
# 3. API key (sent as the header/query the spec declares)- url: https://petstore3.swagger.io/api/v3/openapi.json auth: api-key token-env: PETSTORE_API_KEY
# 4. Custom headers only — for APIs that need multiple auth headers# (e.g. RapidAPI). No `auth:` field needed.- url: https://weatherapi-com.p.rapidapi.com type: openapi headers: X-RapidAPI-Key: "${X_RAPIDAPI_KEY}" X-RapidAPI-Host: weatherapi-com.p.rapidapi.com
# 5. OAuth2 — see next sectionCredential references resolve through a host binding keyed by world, context, principal, connection, and
grant revision, with context access checked on every call. In the local/first-party compatibility
tier, token-env and ${VAR} may then resolve from that world’s credential store (set via
set NAME = ... in chat or via the admin UI), followed by the process environment. Process fallback
is unavailable in shared multi-world or untrusted marketplace deployments. Missing credentials mean
the entry is skipped at world load with a logged warning; the API never appears in the gateway.
A captured realm’s API operations read only the owner’s wallet, through the credential the owner
bound, and never fall back to the process environment.
OAuth2
Section titled “OAuth2”For providers that use the OAuth2 authorization-code flow (HubSpot, Slack, Salesforce, GitHub, Google, etc.). The pack ships only the provider facts (URLs, scopes, identity introspection). Per-deployment client app credentials live in the host’s org vault, keyed by provider name — never in the pack repo and never in any user’s workspace. (The host admin file oauth-apps.yml is retired; credentials are no longer read from disk.)
- url: hubspot-crm.json name: hubspot type: openapi auth: oauth2 oauth2: auth-url: https://app.hubspot.com/oauth/authorize token-url: https://api.hubapi.com/oauth/v1/token scopes: >- crm.objects.contacts.read crm.objects.contacts.write crm.objects.companies.read crm.objects.deals.read identity: url: https://api.hubapi.com/oauth/v1/access-tokens/{token} method: GET auth: path-token account-id-field: hub_id display-name-field: hub_domainoauth2: block fields
| Field | Required | Notes |
|---|---|---|
auth-url |
yes | Provider’s authorize endpoint. |
token-url |
yes | Provider’s token endpoint. |
scopes |
usually | Space-separated scope list. |
client-id / client-secret |
NO in published packs | Power-user fallback only — accepts ${VAR} interpolation. Production setups put these in the host’s org vault (or an individual user’s wallet, which takes precedence) so the pack stays public and credential-free. |
identity |
optional | Introspection block — see below. Without it the connect flow still completes, but the UI shows a generic label instead of the real account. |
identity: block — provider-agnostic introspection
The identity: block tells the host how to call the provider’s /userinfo or /whoami endpoint and pull an accountId + display label out of the JSON response. This is what lets the UI show “Connected as alice@acme.com” rather than just “Connected”.
| Field | Required | Notes |
|---|---|---|
url |
yes | Endpoint URL. May contain the literal {token} placeholder, substituted with the URL-encoded access token (used by HubSpot’s path-token style). |
method |
no | GET (default) or POST. |
auth |
no | How to send the token: bearer (default — Authorization: Bearer <token>), path-token (interpolated into {token}, no header), header:<NAME> (custom header), query:<NAME> (URL query parameter). |
account-id-field |
yes | Top-level JSON field name to read as the stable account identifier. |
display-name-field |
no | Top-level JSON field name for the human-readable label. Falls back to account-id-field if absent or blank in the response. |
Examples:
# Google (bearer token, /userinfo)identity: url: https://www.googleapis.com/oauth2/v3/userinfo account-id-field: email display-name-field: name
# GitHubidentity: url: https://api.github.com/user account-id-field: login display-name-field: name
# Slackidentity: url: https://slack.com/api/auth.test method: POST account-id-field: user_id display-name-field: userWhere the OAuth client credentials live (host-managed, never on disk)
client-id and client-secret for the provider’s Public App belong in the host’s org vault, keyed by provider name. The host manages them through its admin UI or REST; the exact surface is host-specific, but the key is always the provider name:
PUT /api/v1/admin/org-vault/provider/hubspot{"client-id": "12345-abcdef-…", "client-secret": "…"}The provider name (hubspot, slack, …) matches the name: field of the matching apis.yml entry.
Two properties a pack author can rely on:
- Nothing is read from the filesystem. The old
admin/oauth-apps.ymland its per-workspace override are retired, so a pack must never instruct an operator to write credentials to a file. - A user can override the deployment’s app with their own, by putting it in their personal wallet. Packs should not assume the org’s client app is the one in use — which matters for docs that tell a developer how to test against their own provider project.
Lookup order for client_id / client_secret:
- The user’s own wallet (per-user override — encrypted and self-service, replacing the retired per-workspace file)
- The deployment’s org vault (the default everyone gets)
${VAR}from the pack’soauth2.client-id/client-secret(escape hatch for power users)
If none resolve, the provider’s status reports not-configured and Authorize returns an actionable error message instead of silently failing.
End-user UX
End users never paste tokens, IDs, or secrets. Settings → Connected Services → click Authorize → consent on the provider’s page → done. ConnectedAccounts holds the real account label; gateway.<name>.* is live in chat.
Token refresh is automatic — the host’s OAuth2Service rotates expired access tokens using the stored refresh token and writes back any new refresh token the provider issues (HubSpot rotates them on every refresh).

