Skip to main content
Cyrnel coordinates two kinds of modules: Both implement the base Module interface from @cyrnel/sdk.

The Module Registry

Modules are registered with the ModuleService at startup. There are two sources:
  1. Built-in modules, shipped as workspace packages.
  2. Custom modules, loaded from $CYRNEL_DATA_DIR/modules/<name>/. Each directory must contain a module.json:
    version is a required semver string. engines is optional, but when present its cyrnel field is required: a semver range the Cyrnel core version (CYRNEL_CORE_VERSION) must satisfy, or the module is rejected with a 400 error. Omitting engines entirely means “no core version constraint”. main is an ES module exporting instantiate(): AdapterModule or EnvironmentModule. See Writing a custom module.
The registry is reflected to the modules table. Three things can happen during reconciliation:
  • Insert: A registered module that isn’t in the DB yet. Default enabled: true, missing: false.
  • Missing: A row in the DB whose module is no longer registered (e.g. the custom folder was removed). Set missing: true, preserving enabled. The row stays so its config/enablement is preserved if the module reappears.
  • Restore: A missing row whose module is registered again. Clear missing: false. Services recalculate effectivelyEnabled.
POST /modules/reload re-scans $CYRNEL_DATA_DIR/modules/ and reruns reconciliation. Use it after dropping a new custom module into the directory.

Install

Modules can be installed in two modes:

Direct install

POST /modules
The url is a direct download URL to the .tar.zst archive itself. It is ephemeral — passed by the client, never stored. The downloaded archive is extracted, validated for a module.json manifest, registered in memory, and persisted to the database with source = "".

Registry install

POST /modules/install
The source is a registry URL pointing to a metadata API endpoint. The server fetches source, expecting a JSON response with a versioned descriptor:
The server resolves the latestVersion entry by default, or a specific version/range when version is provided in the request body:
If the response includes a hash, the server verifies the downloaded archive content matches it. The source URL is persisted in the database so the module can be re-resolved later for updates. The module starts disabled.

Update

Modules can be updated in two modes, matching the install mode.

Direct update

PATCH /modules/:moduleId
Downloads the new archive from the direct url, backs up the existing module directory, extracts the new archive, registers the updated factory, and updates the database row (including clearing source to ""). If the update fails partway, the backup is restored. After the module is installed, cyrnel regenerates every service that targets this adapter: for each non-missing service, the stored definition content is re-fed through the new adapter’s generateDefinition. Services that succeed get fresh tools immediately. Services that fail are marked stale and cannot be invoked until they are manually synced (via PATCH /services/:id or POST /services/:id/update).

Registry update

POST /modules/:moduleId/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 and the response is { updated: false }. Otherwise the new archive is downloaded, verified, extracted, and registered. Direct-installed modules (source = "") return 409 — use PATCH instead. Same service regeneration behavior as the direct update: every non-missing service is re-parsed through the new adapter. Failures are marked stale. The source column semantics:
  • source = "" — direct-installed item (no registry reference).
  • source = <url> — registry-installed item (stored registry URL).

Activation

Activation = “this module is now ready to take work”. It is per-module-type:
  • Adapters activate eagerly. On startup cyrnel activates every adapter row with enabled = true, missing = false, runs setup({}), then hydrates every enabled service that targets it.
  • Environments activate one-at-a-time. Only the environment whose row is enabled gets activated. Enabling a different environment disables the previously-enabled one in the DB and swaps the active reference.

Draining environments

When the active environment is replaced or disabled, it doesn’t disappear, instead, it becomes draining. New executions go to the newly-active environment (or fail with 503 if none). The draining environment keeps running its in-flight executions; once all of them finish, cyrnel tears it down. This means a disable request with returns immediately, but the actual teardown may happen seconds later.

Configuration and Secrets

Modules declare configSchema and secretsSchema in their code export, not in module.json. Cyrnel stores module configuration as plaintext JSON and module secrets as AES-256-GCM ciphertext 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. Configuration is readable and patchable:
  • GET /modules/:moduleId/config/schema returns the module’s configuration JSON Schema.
  • GET /modules/:moduleId/config returns the currently stored configuration.
  • PATCH /modules/:moduleId/config accepts a JSON Patch array, validates the resulting object against configSchema, persists it, and reloads the active module instance when required.
Secrets are write-only:
  • GET /modules/:moduleId/secrets/schema returns the module’s secrets JSON Schema.
  • PATCH /modules/:moduleId/secrets accepts a JSON Patch array, applies it to the decrypted secret document, validates against secretsSchema, then re-encrypts and persists the result.
There is no endpoint that returns plaintext module secrets. Use configuration for non-sensitive values only; put credentials and tokens in secrets.

Module API

Enabling a missing module returns 409. Direct-installed items (source = "") cannot use POST /modules/:moduleId/update — they return 409. A module record:

Picking the Right Module Type

  • If you need to let user code reach a new kind of end service, write an adapter.
  • If you need to change how code is executed (different language, different sandbox, different permission model), write an environment.
Last modified on August 1, 2026