Appearance
Channel driver implementation reference
This page contains the detailed operational API, adapter example, discovery rules, and migration procedure. Start with the channel-driver overview for the lifecycle and trust model.
The server loads trusted driver packages at startup and runs all enabled channel instances in one process. A driver defines one channel type, such as whatsapp; an instance is one configured account or endpoint of that type. Adding or changing a driver requires a server restart. There is no hot reload or sandboxing in API version 1.
Run and manage channels
Generate and retain a 32-byte data-encryption key:
sh
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"Set the result as CHANNEL_SECRET_KEY. Set CHANNEL_SECRET to a separate long random value for signed channel callback payloads. Then start the application:
sh
docker compose up --buildOpen http://localhost:8080/channels. The catalog page creates and enables instances. The instance page renders the driver's JSON Schema, status, diagnostics, pairing information, and actions. Dangerous actions require confirmation. Disabling an instance stops it but retains its configuration, credentials, and operational data.
The server's channel management endpoints are:
GET /api/channels/host/driversGETandPOST /api/channels/host/instancesGET,PUT, andDELETE /api/channels/host/instances/:idPOST /api/channels/host/instances/:id/actions/:actionGET /api/channels/host/workflowsPOST /api/channels/host/instances/:id/messagesPOST /api/channels/host/callbacks
These administration endpoints follow the application's current trusted-network authentication posture. Do not expose them directly to an untrusted network.
Create a workspace driver
Use a lower-kebab-case type of at most 63 characters. The default TypeScript export must be a ChannelDriverDefinition, and its type must exactly match manifest.yaml:
ts
import type {
ChannelDriverDefinition,
ChannelInstance,
ChannelStatus,
} from "@faimulus/driver-sdk";
const driver: ChannelDriverDefinition = {
type: "example",
displayName: "Example",
configSchema: {
type: "object",
additionalProperties: false,
required: ["workflowId", "endpoint"],
properties: {
workflowId: {
type: "string",
"x-faimulus-control": "workflow",
},
endpoint: { type: "string", minLength: 1 },
},
},
actions: [
{
id: "reconnect",
label: "Reconnect",
description: "Reconnect this channel endpoint.",
},
],
createChannel(context): ChannelInstance {
let status: ChannelStatus = { state: "starting" };
return {
async start() {
status = { state: "ready", message: "Listening." };
await context.publishStatus(status);
},
async stop() {
status = { state: "stopped" };
},
async status() {
return status;
},
async executeAction(action) {
if (action !== "reconnect") throw new Error("Unknown action");
},
async handleCompletion(completion) {
// Correlate completion.messageId with durable driver state and deliver output.
void completion;
},
};
},
};
export default driver;Driver lifecycle and context
The server validates configuration against configSchema before storing it. enabled belongs to the channel runtime and must not be part of driver configuration. Configuration updates stop and recreate the instance; the driver should acquire resources in start() and release them in stop().
The ChannelDriverContext is scoped to one instance:
instanceIdis the stable account namespace.configis the validated JSON configuration.storeis durable, namespaced JSON key/value storage.secretsis durable, namespaced AES-256-GCM encrypted string storage.dataDirectoryis the only location for driver-owned operational databases.listWorkflows()supplies workflow choices.submitMessage()starts an idempotent workflow run and returns its run ID and status.resolveInteractionRequest()submits an aggregated response to a suspended run.publishStatus()andpublishEvent()expose JSON-only state to the server.signalis aborted when the instance is being stopped.
Submission input, action input/results, events, statuses, and completion data must remain JSON-serializable. Persist a message-to-run correlation before or immediately after submitMessage(). Treat handleCompletion() as idempotent: the channel runtime deduplicates callbacks, but retry-safe delivery is still required around process failures.
Drivers may publish outboundTargetSchema and outboundMessageSchema and implement sendMessage() to start conversations. The server validates both JSON values and deduplicates the caller's requestId before returning the driver message receipt. Drivers that implement handleInteractionRequest() must durably correlate the request with its conversation before sending questions. Grouped questions are returned as one object keyed by question ID. Channel requests always return to their originating instance and default to private and non-broadcasted. A concurrent direct or channel response is atomically claimed, so the first valid response wins.
Status presentation supports instructions, warnings, identities, and PNG/SVG QR images. Action descriptors can include description, dangerous, and an input JSON Schema. The generic UI currently renders object properties whose types are string, number/integer, boolean, enum, or arrays of primitive values.
An instance start or action failure affects that instance only. In contrast, in strict discovery mode (the default), an invalid manifest, unsafe entrypoint, duplicate type, or load failure prevents server startup so a deployment cannot silently omit a driver.
Add a trusted workspace driver
Application-owned channel drivers use the shared workspace TypeScript contract:
text
workspace/drivers/channel/example/
|-- manifest.yaml
|-- package.json
`-- driver.tsLock dependencies for every driver kind at the shared driver root:
text
workspace/drivers/
|-- package.json
|-- package-lock.json
|-- memory/
|-- provider/
`-- channel/Mount the workspace read-only:
yaml
services:
server:
environment:
WORKSPACE_ROOT: /app/workspace
volumes:
- type: bind
source: ./workspace
target: /app/workspace
read_only: trueAt startup, Faimulus installs locked production dependencies into a content-addressed cache below RUN_DATA_DIR. An unchanged dependency hash reuses the existing cache. Set WORKSPACE_DRIVER_DEPENDENCY_ROOT only to use a preinstalled tree instead. Driver and dependency lifecycle code is fully trusted and runs with the server's process permissions. Registry access is required when a new dependency hash is first installed.