> ## Documentation Index
> Fetch the complete documentation index at: https://actelos.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Services

> Installed services, their tools, configuration, and secrets

A **service** is an installed integration, it bundles metadata, a set of tools,
configuration schema, a secrets schema, and the [adapter module](/cyrnel/docs/adapters-modules)
that knows how to talk to the underlying end service.

## Service Shape

```ts theme={null}
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](/cyrnel/docs/adapters-modules) 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`

```json theme={null}
{
  "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`):

```json theme={null}
{ "id": "petstore" }
```

`409` on duplicate `id`.

### Registry install

`POST /services/install`

```json theme={null}
{
  "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:

```ts theme={null}
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`):

```json theme={null}
{ "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`

```json theme={null}
{ "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`

```json theme={null}
{}
```

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.

```json theme={null}
// 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`

```json theme={null}
{ "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/:serviceId` → `204 No Content`

Deletes the row (cascading to tools, configuration, secrets) and calls
`adapter.dehydrateService(serviceId)`.

## List & Get

| Method | Path                   | Notes                                              |
| ------ | ---------------------- | -------------------------------------------------- |
| `GET`  | `/services`            | Filterable by `query`, `limit`, `enabled`, `stale` |
| `GET`  | `/services/:serviceId` | Full 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.

| Method  | Path                                 | Returns                      |
| ------- | ------------------------------------ | ---------------------------- |
| `GET`   | `/services/:serviceId/config/schema` | `{ configSchema }`           |
| `GET`   | `/services/:serviceId/config`        | `{ config }`                 |
| `PATCH` | `/services/:serviceId/config`        | Apply an RFC 6902 JSON Patch |

`PATCH` body is an array of JSON Patch operations:

```json theme={null}
[
  { "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`.

| Method  | Path                                  | Returns                      |
| ------- | ------------------------------------- | ---------------------------- |
| `GET`   | `/services/:serviceId/secrets`        | `{ present: string[] }`      |
| `GET`   | `/services/:serviceId/secrets/schema` | `{ secretsSchema }`          |
| `PATCH` | `/services/:serviceId/secrets`        | Apply 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.

<Warning>
  If `CYRNEL_SECRETS_KEY` is missing or the wrong size, both reading and
  writing secrets fail with `500`. See
  [Security](/cyrnel/docs/security#secrets).
</Warning>

## 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.
