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

interface ServiceDefinition {
  name: string;
  description: string;
  configSchema: JSONSchema;
  secretsSchema: JSONSchema;
  tools: ToolDefinition[];
  adapterDomain: Record<string, unknown>;
}
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_$]*).
  • 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
{
  "id": "petstore",
  "url": "https://example.com/petstore.openapi.json",
  "adapter": "openapi"
}
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):
{ "id": "petstore" }
409 on duplicate id.

Registry install

POST /services/install
{
  "source": "https://registry.example.com/services/petstore",
  "adapter": "openapi",
  "id": "petstore"
}
The source is a registry URL pointing to a metadata API endpoint. The server fetches source, expecting a JSON response:
interface ServiceRegistryResponse {
  downloadUrl: string;   // URL to the definition file
  hash?: string;         // SHA-256 of the definition text (skip + integrity)
  id?: string;           // default service id (client can override)
  adapter?: string;      // default adapter module id (client can override)
}
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):
{ "id": "petstore" }
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": "https://example.com/petstore-v2.openapi.json" }
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.
// Response 200
{ "id": "petstore", "updated": true }
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
{ "enabled": true }
Enable validates configuration and secrets against their schemas (applying defaults), then calls 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/:serviceId204 No Content Deletes the row (cascading to tools, configuration, secrets) and calls adapter.dehydrateService(serviceId).

List & Get

MethodPathNotes
GET/servicesFilterable by query, limit, enabled, stale
GET/services/:serviceIdFull record (minus adapterDomain)
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.
MethodPathReturns
GET/services/:serviceId/config/schema{ configSchema }
GET/services/:serviceId/config{ config }
PATCH/services/:serviceId/configApply an RFC 6902 JSON Patch
PATCH body is an array of JSON Patch operations:
[
  { "op": "replace", "path": "/baseUrl", "value": "https://api.example.com" },
  { "op": "add", "path": "/timeoutMs", "value": 15000 }
]
The server applies the patch, validates the result against 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 using CYRNEL_SECRETS_KEY.
MethodPathReturns
GET/services/:serviceId/secrets{ present: string[] }
GET/services/:serviceId/secrets/schema{ secretsSchema }
PATCH/services/:serviceId/secretsApply an RFC 6902 JSON Patch
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.
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 June 24, 2026