- Adapter modules translate environment invocations into calls to end services.
- Environment modules execute the code clients submit.
Module interface from @cyrnel/sdk.
The Module Registry
Modules are registered with theModuleService at startup. There are two
sources:
- Built-in modules, shipped as workspace packages.
-
Custom modules, loaded from
$CYRNEL_DATA_DIR/modules/<name>/. Each directory must contain amodule.json:versionis a required semver string.enginesis optional, but when present itscyrnelfield is required: a semver range the Cyrnel core version (CYRNEL_CORE_VERSION) must satisfy, or the module is rejected with a 400 error. Omittingenginesentirely means “no core version constraint”.mainis an ES module exportinginstantiate(): AdapterModuleorEnvironmentModule. See Writing a custom module.
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, preservingenabled. 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 recalculateeffectivelyEnabled.
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
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
source is a registry URL pointing to a metadata API endpoint. The server
fetches source, expecting a JSON response with a versioned descriptor:
latestVersion entry by default, or a specific
version/range when version is provided in the request body:
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
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
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, runssetup({}), 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 with503 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 declareconfigSchema 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/schemareturns the module’s configuration JSON Schema.GET /modules/:moduleId/configreturns the currently stored configuration.PATCH /modules/:moduleId/configaccepts a JSON Patch array, validates the resulting object againstconfigSchema, persists it, and reloads the active module instance when required.
GET /modules/:moduleId/secrets/schemareturns the module’s secrets JSON Schema.PATCH /modules/:moduleId/secretsaccepts a JSON Patch array, applies it to the decrypted secret document, validates againstsecretsSchema, then re-encrypts and persists the result.
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.