An adapter module is the part of cyrnel that knows how to talk to a
specific kind of end service. It is responsible for:
- Parsing definitions. Turning whatever the user supplied at install
time (an OpenAPI document, a gRPC proto, a custom JSON descriptor, …)
into a
ServiceDefinition cyrnel can store.
- Holding runtime state. Storing per-service config and secrets, and
any
adapterDomain metadata produced at install time.
- Invoking tools. Translating a generic
{ serviceId, toolId, parameters } invocation into a real call to the
underlying service.
Adapter modules implement the SDK AdapterModule
interface.
Lifecycle
interface AdapterModule extends Module {
generateDefinition(input: string): Promise<ServiceDefinition>;
hydrateService(state: ServiceState): Promise<void>;
dehydrateService(id: string): Promise<void>;
invoke(input: InvokeInput): Promise<unknown>;
}
-
setup({}) is called once when the adapter is activated.
-
generateDefinition(content) is called during POST /services
(direct install), POST /services/install (registry install),
PATCH /services/:serviceId (direct update), and
POST /services/:serviceId/update (registry update). Pure: given the
same definition string, should produce the same ServiceDefinition.
-
hydrateService(state) is called whenever a service that targets this
adapter is enabled, has its config or secrets patched, or this adapter is
activated while the service is already enabled. Receives a snapshot:
interface ServiceState {
id: string;
adapterDomain: Record<string, unknown>;
tools: Record<string, ToolState>;
config: Record<string, unknown>;
secrets: Record<string, unknown>;
}
The adapter is expected to store this and use it on subsequent
invoke calls.
-
dehydrateService(id) is called when a service is disabled, deleted,
or updated. The adapter should drop any state it kept for that service.
-
invoke(input) is called once per tool call. Must throw for an
unknown (serviceId, toolId) pair or any transport-level failure.
-
teardown() is called when the adapter is deactivated. Should release
any pooled resources.
Security Model
Adapter modules are the most security-sensitive part of cyrnel. They:
- Receive decrypted secrets via
ServiceState.secrets.
- Make outbound network requests (or open files, or send messages,
etc.) to end services on behalf of users.
- Run in the host Node.js process with full host permissions, they are
not sandboxed.
Concretely, the trust boundary is “the operator of the cyrnel server” decides
which adapters to enable. Every enabled adapter can:
- Read every secret of every service it owns.
- Reach anything reachable from the host.
- Persist or transmit anything it receives.
Implications
- Treat the contents of
$CYRNEL_DATA_DIR/modules/ as code with full host
privileges. Use the same review you would use for any other server
code. A malicious adapter trivially exfiltrates secrets.
- Don’t enable adapters you can’t read. “Plug-and-play” is convenient
but not safe.
- Don’t reuse
CYRNEL_SECRETS_KEY across environments. A leaked key,
combined with a copy of data.db, decrypts every secret of every service.
- Be careful with
adapterDomain. It is persisted in plaintext in the
services table. If the adapter places URLs, header templates, or other
identifying metadata there, treat that table as sensitive too.
What an adapter must not do
- Persist secrets outside
ServiceState. All secret retention must be
in-memory; if the process restarts, hydrateService is called again.
Writing them to disk silently breaks the encryption guarantee cyrnel is
trying to give the operator.
- Drop authentication on retry. If a request fails, retry with the same
credentials, not without them.
- Hold per-service state in a way that survives
dehydrateService.
See Security for the broader picture.
Authoring
See Writing a custom adapter module
for the file layout, a skeleton, and a full working example.
AdapterModule covers the interface contract
in full. Last modified on June 19, 2026