A Node.js/TypeScript application that fetches messages from Telegram, Email (IMAP), and Slack. CommunicationChannel config controls transport reachability and optional sender pre-filters. Workflow approval identity verification is a separate Profile UI flow and is enforced only by workflow approval/trigger handling.
Watches CommunicationChannel CRDs in Kubernetes, filtered by hostRef. Auto-restarts when CRDs change.
Reads channel config from CLERUM_CHANNEL environment variable. No Kubernetes access required.
- Loads config: From Kubernetes CRDs (production) or env var (dev mode)
- Extracts transport filters: Telegram
userIdsare an optional legacy pre-filter; Email requiresemailson every group; Slack requires eitheruserIds+workspaceId(stable identity — the only form usable for workflow approval decisions) or legacyuserNames. The CRD enforces this (required/anyOf) andchannelConfigValidation.tsre-checks it at load - Polls channels: Connects to messaging platforms and fetches new messages
- Filters transport input: Applies configured sender pre-filters before forwarding messages to the runtime
- Delegates workflow identity: Workflow approval commands are authorized later through verified approval-medium accounts and workflow permissions
| Variable | Required | Default | Description |
|---|---|---|---|
CLERUM_DEV_MODE |
No | false |
Enable dev mode (read config from env var) |
CLERUM_CHANNEL |
Dev mode | - | Channel config JSON (dev mode only) |
CLERUM_HOST_REF |
Prod mode | - | Filter CommunicationChannels by this hostRef |
CLERUM_NAMESPACE |
No | "" (all) | Namespace to watch (production mode) |
CLERUM_TELEGRAM_BOT_TOKEN |
Dev mode | - | Telegram Bot API token. Dev mode only — production reads it from spec.credentialsSecretRef (key telegram-bot-token). |
CLERUM_SLACK_BOT_TOKEN |
Dev mode | - | Slack Bot OAuth token (xoxb-...). Dev mode only — production reads it from spec.credentialsSecretRef (key slack-bot-token). |
CLERUM_EMAIL_IMAP_HOST |
No | - | IMAP server host |
CLERUM_EMAIL_IMAP_PORT |
No | 993 |
IMAP server port |
CLERUM_EMAIL_SMTP_HOST |
No | falls back to CLERUM_EMAIL_IMAP_HOST |
SMTP host for the send path. Set this when your outbound host differs from IMAP. |
CLERUM_EMAIL_SMTP_PORT |
No | 587 |
SMTP server port (send path) |
CLERUM_EMAIL_USERNAME |
Dev mode | - | IMAP username. Dev mode only — production reads it from spec.credentialsSecretRef (key email-username). |
CLERUM_EMAIL_PASSWORD |
Dev mode | - | IMAP password. Dev mode only — production reads it from spec.credentialsSecretRef (key email-password). |
CLERUM_POLL_INTERVAL_SECONDS |
No | 2 |
Seconds between polling cycles. Telegram messages land in an in-memory queue via grammY long-polling and pollCycle only drains it. Slack and Email are fetched during the cycle (Slack conversations.history, IMAP), so this interval bounds their delivery latency. |
CLERUM_ENABLE_RESPONSE_ATTACHMENTS |
No | true |
Enable response attachment delivery. Set to false to disable non-workflow response attachments. |
CLERUM_ATTACHMENT_MAX_COUNT |
No | 3 |
Max attachments delivered per response |
CLERUM_ATTACHMENT_MAX_BYTES |
No | 52428800 |
Max decoded bytes per attachment |
CLERUM_PROFILE_UI_URL |
No | - | Base URL of the Profile UI. When set, the unresolved-sender notice ends with this link so an unverified Slack sender can find the account they need to link; when unset the notice still sends, just without a link. |
Attachment delivery behavior:
- Telegram: JPEG attachments are sent as photos. Workflow result documents and supported internal generated artifacts are sent as documents.
- Slack: workflow result documents and supported internal generated artifacts are uploaded as real files (
files.uploadV2). Any attachment outside those two categories is not delivered; the text reply then carries a note counting the undeliverable attachments. - Email: the text reply is sent with an attachment-availability note; no attachment is delivered.
For local development without Kubernetes, use dev mode:
export CLERUM_DEV_MODE=true
export CLERUM_CHANNEL='{
"hostRef": "chatllm",
"telegram": [
{
"channelId": "telegram1",
"chatType": "private",
"userIds": ["123456789"]
}
]
}'
export CLERUM_TELEGRAM_BOT_TOKEN="your-token"
npm startThe CLERUM_CHANNEL JSON matches the CommunicationChannel CRD spec:
{
"hostRef": "chatllm",
"telegram": [
{
"channelId": "telegram1",
"chatType": "private",
"userIds": ["123456789", "987654321"]
}
],
"email": [
{
"channelId": "INBOX",
"emails": ["user@example.com"]
}
],
"slack": [
{
"channelId": "C01234567",
"workspaceId": "T01234567",
"userIds": ["U01234567", "U07654321"]
}
]
}Slack groups must supply either userIds + workspaceId or the legacy userNames. Only the userIds + workspaceId form is a valid workflow-approval identity; userNames is a legacy chat-only allowlist. When userIds are set, channelId must be a stable Slack channel ID (C…, D…, or G…) and workspaceId a team ID (T…).
The channel-reader needs cluster-wide list and watch on communicationchannels.clerum.io (via a ClusterRole — the CRD itself is namespaced, and CLERUM_NAMESPACE="" means watch all namespaces), plus get on Secrets in the channels namespace — at adapter init it reads spec.credentialsSecretRef through the Kubernetes API to resolve the bot tokens:
# ClusterRole
rules:
- apiGroups: ['clerum.io']
resources: ['communicationchannels']
verbs: ['list', 'watch']
---
# Role in the `channels` namespace
rules:
- apiGroups: ['']
resources: ['secrets']
verbs: ['get']Both are shipped in deploy/base/cluster-wide/clusterroles.yaml and deploy/base/channels/rbac.yaml.
apiVersion: clerum.io/v1alpha1
kind: CommunicationChannel
metadata:
name: my-channels
spec:
hostRef: chatllm # Must match CLERUM_HOST_REF
# In production mode, credentials are read ONLY from this Secret (in the
# `channels` namespace). The CLERUM_*_TOKEN env vars are dev-mode only.
credentialsSecretRef:
name: my-channels-credentials
telegram:
- channelId: telegram1
chatType: private
userIds:
- '123456789'
email:
- channelId: INBOX
emails:
- user@example.com
slack:
- channelId: C01234567 # stable channel ID required when userIds are set
workspaceId: T01234567
userIds:
- U01234567When you create, update, or delete this CRD, the channel-reader automatically detects the change and restarts.
npm install
npm run build
export CLERUM_DEV_MODE=true
export CLERUM_CHANNEL='{"hostRef":"dev","telegram":[{"channelId":"test","chatType":"private","userIds":["123"]}]}'
export CLERUM_TELEGRAM_BOT_TOKEN="your-token"
npm startThe CRD watcher tries loadFromCluster() first and falls back to loadFromDefault() (src/k8sClient.ts), so production mode can also run off-cluster against whatever ~/.kube context is current — the ServiceAccount token is used only when the process runs as a pod. Credentials are resolved exclusively from the Secret named by CommunicationChannel.spec.credentialsSecretRef.name in the channels namespace; the CLERUM_TELEGRAM_BOT_TOKEN / CLERUM_SLACK_BOT_TOKEN / CLERUM_EMAIL_USERNAME / CLERUM_EMAIL_PASSWORD env vars are read by the dev-mode resolver only and are ignored in production. Running it locally therefore requires a kubeconfig whose user can read the CommunicationChannels and the credentials Secret.
# Uses the in-cluster ServiceAccount when run as a pod, otherwise the current kubeconfig context.
export CLERUM_HOST_REF="chatllm"
npm startFor local iteration, use dev mode above.
# Build image
docker build -t clerum/channel-reader:latest .
# Run in dev mode
docker run --rm \
-e CLERUM_DEV_MODE=true \
-e CLERUM_CHANNEL='{"hostRef":"dev","telegram":[{"channelId":"test","chatType":"private","userIds":["123"]}]}' \
-e CLERUM_TELEGRAM_BOT_TOKEN="your-token" \
clerum/channel-reader:latestProduction mode inside a container needs Kubernetes credentials: as a pod it picks up the mounted ServiceAccount token, and any other runtime must have a kubeconfig mounted for the loadFromDefault() fallback to find. In the cluster, host-context-controller creates the per-Host channel-reader-<host> Deployment for you — see below.
There is no standalone channel-reader Deployment to apply. The legacy static Deployment was retired in favor of per-Host channel-reader-<host> Deployments created by host-context-controller (HCC) from the Host CR. The only static pieces are the clerum-channel-reader ServiceAccount and the clerum-channel-reader-config ConfigMap, which the per-Host pods reference — both live in deploy/base/channels/ in the channels namespace and ship with the platform deploy.
- Install CRDs (if not already), from the repo root:
helm install clerum-crds charts/clerum-crds- Deploy the platform (namespaces, RBAC, ServiceAccount, ConfigMap, HCC), from the repo root:
kubectl apply -k deploy/overlays/minikube # or your own overlay-
Create the credentials Secret in the
channelsnamespace, holding the keys channel-reader reads (telegram-bot-token,slack-bot-token,email-username,email-password), then reference it from the CommunicationChannel viaspec.credentialsSecretRef.name. -
Create a CommunicationChannel with a
hostRefmatching an existingHost(see the example above). HCC reconciles the per-Host channel-reader Deployment; the reader picks up the CRD and restarts on change. -
Check logs:
# `app=channel-reader` matches every per-Host reader; app.kubernetes.io/name
# is the per-Host value `channel-reader-<host>`.
kubectl logs -n channels -l app=channel-reader -fchannel-reader/
├── src/
│ ├── main.ts # Entry point, polling loop, dev/prod mode
│ ├── config.ts # Environment configuration
│ ├── types.ts # Shared TypeScript types
│ ├── k8sClient.ts # Kubernetes CRD watcher (production mode)
│ ├── credentials.ts # Secret-backed + dev-mode credential resolvers
│ ├── channelConfigValidation.ts # CommunicationChannel spec validation
│ ├── handoffServer.ts # Inbound handoff HTTP listener
│ ├── rpcClient.ts # mcp-host runtime client
│ ├── progressClient.ts # Task progress streaming
│ ├── progressFormatter.ts # Progress rendering
│ ├── notificationDeliveryClient.ts
│ ├── telegramCallbackData.ts # Telegram callback payload codec
│ ├── workflowApproval*.ts # Workflow approval coordination/decisions
│ └── channels/
│ ├── index.ts # Channel adapter exports
│ ├── base.ts # Shared utilities
│ ├── telegram.ts # Telegram adapter (grammY)
│ ├── email.ts # Email adapter (ImapFlow)
│ ├── slack.ts # Slack adapter (@slack/web-api)
│ ├── slackVerification.ts
│ ├── telegramDelivery.ts
│ ├── telegramOperationalMessage.ts
│ ├── telegramProviderTarget.ts
│ └── telegramVerification.ts
├── Dockerfile
├── package.json
└── tsconfig.json
Kubernetes manifests are not vendored here — the static ServiceAccount/ConfigMap and RBAC live in deploy/base/channels/, and the per-Host Deployment is generated by host-context-controller.