Service Shape
id: the service identifier the client chose at install time. Must be a valid identifier ([A-Za-z_$][A-Za-z0-9_$]*).version: semver version of the service definition. Defaults to"0.0.0"for direct installs; set from the registry descriptor for registry installs.hash: SHA-256 of the definition file used at install/update time.source: the registry URL the service was installed from.""means the service was direct-installed (no registry reference).adapter: the id of the adapter module that parsed the definition and will handle invocations.enabled: whether the service is active. Disabling a service hides its tools from execution.stale: whentrue, the service’s tools were generated by a previous version of the adapter and could not be regenerated after the last module update. Stale services cannot be enabled or invoked until they are manually synced (viaPOST /services/:id/sync).
adapterDomain is a free-form bag the adapter uses to carry
adapter-specific state across hydration boundaries (e.g. the OpenAPI adapter
stores base URLs and operation details here). Treat it as opaque from the
outside.
Install a Service
Services can be installed in two modes.Direct install
POST /services
url is a direct download URL to the definition file itself. It is
ephemeral — passed by the client, never stored. The server downloads the
definition, hashes it, calls adapter.generateDefinition(content), validates
tool identifiers, and inserts the service with source = "". The service
starts disabled.
Response (201):
409 on duplicate id.
Registry install
POST /services/install
source is a registry URL pointing to a metadata API endpoint. The server
fetches source, expecting a versioned JSON response:
version request field (optional) selects a semver range; omitted or
"latest" resolves to latestVersion. If the response includes a hash, the server verifies the downloaded content
matches it. The effective id is merged from the request body and the registry
response (body wins over registry default). The adapter is resolved as follows:
an explicit adapter in the body wins; otherwise the server ranks installed
adapter modules against the definition kind (see Definition kinds &
adapter compatibility) and selects the best
compatible and active adapter. The source URL is persisted in the database so
the service can be re-resolved later for updates. The service starts disabled.
400 when the descriptor has no kind or when no installed adapter is
compatible and active for the resolved kind (and the body did not provide an
explicit adapter override). Resolve candidates first with
GET /services/install/adapters?kind=<kind>.
If a version entry declares an icon, the server downloads the image bytes
through the same SSRF-guarded egress path as definitions, verifies the
SHA-256 hash, and only accepts png/webp content sniffed from the file magic.
The icon is stored server-side and served from
GET /services/:serviceId/icon with an ETag for caching. Icon failures are
non-fatal: the service installs without an icon. Direct installs and direct
updates (PATCH) never carry an icon. On registry updates the icon is
re-fetched when its hash changes and cleared when the registry no longer
declares one; a failed re-fetch is non-fatal and preserves the previously
stored icon.
Recommended icon properties: a square 256×256 px image (png or webp, with
transparency if needed) no larger than 256 KB. Icons are displayed at 40×40 px
in the web UI.
Response (201):
409 on duplicate id. Either the request body or the registry must provide
an id; an adapter must be provided explicitly or resolvable from the
definition kind with a compatible, active adapter module, otherwise the server
returns 400.
Update a Service
Services can be updated in two modes, matching the install mode.Direct update
PATCH /services/:serviceId
url, hashes it, re-parses
through the adapter, replaces the stored tools (preserving per-tool enabled
flags by name), and clears source to "". The service is set back to
enabled: false and dehydrated from the adapter. Returns { id, updated: boolean }.
Registry update
POST /services/:serviceId/update
source URL by fetching the registry metadata again.
If the registry returns a hash that matches the stored hash, the update is
skipped. Otherwise the new definition is downloaded, verified, re-parsed, and
stored. Direct-installed services (source = "") return 409 — use PATCH
instead. The service is set back to enabled: false and dehydrated from the
adapter.
The source column semantics:
source = ""— direct-installed item (no registry reference).source = <url>— registry-installed item (stored registry URL).
Sync a Service
POST /services/:serviceId/sync
Re-registers the service from its stored definition content without
re-downloading. This is used to reconcile the service with its adapter
after the adapter was updated or the service was marked stale. The service
is set back to enabled: false after sync.
stale: true) must be synced before it can be enabled or
invoked. Direct-installed and registry-installed services both support
syncing, only a service with no stored definition content returns 409.
Enable / Disable a Service
POST /services/:serviceId/enabled
adapter.hydrateService(state). Keys stored from an
older schema version that are no longer defined by the current schema are
tolerated: they are excluded from validation and not passed to the adapter.
If hydration throws, the enabled flag is rolled back. Stale services
(stale = true) cannot be enabled, use POST /services/:id/sync to sync
the service first.
Disable calls adapter.dehydrateService(serviceId) and best-efforts logs
any failure.
Delete a Service
DELETE /services/:serviceId → 204 No Content
Deletes the row (cascading to tools, configuration, secrets) and calls
adapter.dehydrateService(serviceId).
List & Get
The list response strips
configSchema, secretsSchema, tools, and
adapterDomain, fetch a single service for those.
All list endpoints return the paginated envelope { items, nextCursor, hasMore }. Pass nextCursor back as cursor for the next page; see
Pagination for limit semantics and error codes.
Configuration
Per-service configuration is JSON-Schema validated (configSchema on
the service) and stored as a JSON blob.
PATCH body is an array of JSON Patch operations:
outdated pointers. Stored values that are no
longer defined by the schema are preserved; adding new schema-disallowed
keys is rejected, and remove operations targeting paths that do not
exist are ignored. If the service is currently enabled, cyrnel
re-hydrates it on the adapter so the change takes effect immediately.
outdated lists RFC 6901 pointers of stored values that are not part of
the current schema (for example a key renamed in a newer service version).
These values are excluded from the config payload and from adapter
hydration, but they remain stored and are only removed through an explicit
remove operation. Pointers into array items include the item index and
key (for example /list/items/0/extra); removing the whole array via its
pointer (for example /list) also clears any outdated items it contains.
Secrets
Per-service secrets behave like configuration, but the stored payload is encrypted with AES-256-GCM usingCYRNEL_SECRETS_KEY. When a record is
read that was encrypted with a previous key (during rotation), it is
silently re-encrypted with the current primary key and persisted.
During a key rotation, both CYRNEL_SECRETS_KEY (new key) and
CYRNEL_SECRETS_PREVIOUS_KEYS (old key, comma-separated if multiple) must
be set. If the previous keys are missing and an existing record was encrypted
under the old key, decryptSecrets throws 500, the payload’s key ID
won’t match any key in the ring, and the fallback has nowhere to go.
Secret values never leave the server in plaintext through the API. The
presence endpoint only exposes which paths have values set (without
revealing the values themselves), plus
outdated pointers for stored
values no longer defined by the schema. Patches operate against the
decrypted document in memory; the result is re-encrypted before being
written back.
How Services Are Used at Execution Time
When client code calls a tool:- The environment forwards the call to the host as an
invokeToolbinding. - The
ModuleServicejoins theservicesandtoolstables on(serviceId, toolId). - It rejects the call if the service or the tool is missing (
404), if the service is stale (409), if the service is disabled (409), or if the tool is disabled (409). - Otherwise it looks up the adapter from
services.adapterand callsadapter.invoke({ serviceId, toolId, parameters }).
adapterDomain, configuration, and secrets it received
during hydration.