Skip to main content
A service is an installed integration, it bundles metadata, a set of tools, configuration schema, a secrets schema, and the adapter module that knows how to talk to the underlying end service.

Service Shape

On disk, cyrnel extends that with:
  • 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: when true, 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 (via POST /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
The 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
The source is a registry URL pointing to a metadata API endpoint. The server fetches source, expecting a versioned JSON response:
The 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
Downloads the new definition from the direct 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
Re-resolves the stored 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.
A stale service (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
Enable validates configuration and secrets against their schemas (applying defaults), then calls 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:
The server applies the patch, persists the merged object, and returns the stored payload plus any 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 using CYRNEL_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.
If CYRNEL_SECRETS_KEY is missing or the wrong size, both reading and writing secrets fail with 500. See Security.

How Services Are Used at Execution Time

When client code calls a tool:
  1. The environment forwards the call to the host as an invokeTool binding.
  2. The ModuleService joins the services and tools tables on (serviceId, toolId).
  3. 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).
  4. Otherwise it looks up the adapter from services.adapter and calls adapter.invoke({ serviceId, toolId, parameters }).
The adapter is responsible for translating that into a real call to the end service using the adapterDomain, configuration, and secrets it received during hydration.
Last modified on August 25, 2026