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 and adapter are merged from the request body
and registry response (body wins over registry default). The source URL is
persisted in the database so the service can be re-resolved later for updates.
The service starts disabled.
Response (201):
409 on duplicate id. Either the request body or the registry must provide
an id and adapter or 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). 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.
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:
configSchema,
fills in any missing defaults from the schema, and persists the merged
object. If the service is currently enabled, cyrnel re-hydrates it on the
adapter so the change takes effect immediately.
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). 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.