# === Source: README.md ===
OpenClaw Channel for TrueConf Server
Connect OpenClaw to the corporate TrueConf Server messenger.
English /
Русский
> [!CAUTION]
> This plugin acts as a gateway interface for interacting with OpenClaw. TrueConf does not encourage the use of OpenClaw, does not recommend deploying it within corporate infrastructure, and assumes no responsibility for the operation of OpenClaw, associated AI agents, external services, or third-party components.
>
> Use of this plugin and connection to OpenClaw are performed solely at the user’s own discretion and risk. Before deploying it in a corporate network, users should carefully review the relevant information security risks, including data leakage, unauthorized access, prompt injection, execution of unintended commands, compromise of API keys, and unpredictable behavior of AI agents:
>
> - [OpenClaw threats: assessing the risks, and how to handle shadow AI](https://www.kaspersky.com/blog/moltbot-enterprise-risk-management/55317/)
> - [Don’t Let the Claw Grip Your Hand: A Security Analysis and Defense Framework for OpenClaw](https://arxiv.org/html/2603.10387v1)
>
> This plugin should be used only after conducting a risk assessment, testing it in an isolated environment, restricting access privileges, configuring monitoring, and implementing appropriate measures to protect corporate infrastructure.
> [!IMPORTANT]
> This project is still in development. That said, we’d greatly appreciate it if you gave it a try. If you run into any problems, please feel free to open an [issue](https://github.com/TrueConf/trueconf-openclaw-channel/issues) with a detailed description.
## How it works
After installation, the OpenClaw AI agent communicates with users via TrueConf — messages sent to the bot are forwarded to the AI agent, and responses are delivered back to the chat.
```
[TrueConf client] -> [TrueConf Server] -> [OpenClaw + plugin] -> [LLM]
you type Chatbot Connector receives message generates
a message forwards to LLM response
```
## Agent Capabilities
With this channel, OpenClaw can:
- **Work in group chats** — in a group the bot responds only when mentioned with `@` or when someone replies to its message (see the [Group Chats](#group-chats) section)
- **Work across federation** — a bot on one TrueConf server replies to users on other servers via federation (see the [Cross-Server Federation](#cross-server-federation) section)
- **Survive transient disconnects** — outbound messages park during WS reconnects and token rotations, then drain after re-auth (no manual retry, nothing lost)
## Requirements
- **OpenClaw** >= 2026.3.22
- **TrueConf Server** >= 5.5.3
- **Bot account** on TrueConf Server
## Installation
Prerequisites: `node >= 22.14`, `npm`, `openclaw` (`npm install -g openclaw@latest`).
### From npm (recommended)
```bash
openclaw plugins install @trueconf-community/trueconf-openclaw-channel
npx -y -p @trueconf-community/trueconf-openclaw-channel trueconf-setup
openclaw gateway
```
`trueconf-setup` is the channel setup wizard: it prompts for the server URL, bot username and password, verifies TLS and OAuth, and writes the result to `~/.openclaw/openclaw.json`.
### From source
```bash
git clone https://github.com/TrueConf/trueconf-openclaw-channel.git
cd trueconf-openclaw-channel
npm install
npm run build
openclaw plugins install -l .
npm run setup
openclaw gateway
```
### Docker / Ansible / CI
Set environment variables before running `trueconf-setup` — the wizard will detect them and skip all prompts:
```bash
export TRUECONF_SERVER_URL=tc.example.com
export TRUECONF_USERNAME=bot_user # account login only, without the server address
export TRUECONF_PASSWORD=secret
export TRUECONF_USE_TLS=true # optional; default — auto-detect
export TRUECONF_PORT=443 # optional; any port 1-65535
export TRUECONF_ACCEPT_UNTRUSTED_CA=true # required if the cert is self-signed and the wizard should download it
openclaw plugins install @trueconf-community/trueconf-openclaw-channel
npx -y -p @trueconf-community/trueconf-openclaw-channel trueconf-setup
openclaw gateway
```
Without `TRUECONF_ACCEPT_UNTRUSTED_CA=true`, on a self-signed certificate the wizard fails with `Self-signed cert detected; set TRUECONF_ACCEPT_UNTRUSTED_CA=true to auto-download chain`.
### Re-running setup
You can run `trueconf-setup` again. You can change and save specific field values as needed.
### Self-signed certificates
If your TrueConf Server uses a self-signed or internal-CA certificate, the wizard offers three trust paths (in decreasing order of safety):
1. **`caPath`** — point at a CA file from your admin. Set `"caPath": "/path/to/server-ca.pem"` in the config or pick "Specify path to a root CA certificate" in the interactive wizard.
2. **`NODE_EXTRA_CA_CERTS` / system trust** — add the CA to the OS or Node trust store; the probe then sees the cert as trusted and `caPath` isn't needed.
3. **`tlsVerify: false`** — last resort, skips verification for this TrueConf Server only. Pick "Disable TLS certificate verification for this TrueConf Server" in the wizard, set `"tlsVerify": false` in the config, or pass `TRUECONF_TLS_VERIFY=false` to the headless setup.
See [TLS trust for TrueConf Server](#tls-trust-for-trueconf-server) below for the full guide and security caveats — including why you should never set `NODE_TLS_REJECT_UNAUTHORIZED=0`.
### Verifying the channel
The logs should contain:
```
[trueconf] Connected and authenticated
```
Open the TrueConf client, find the bot in your contacts, and send it a message.
### Setting this up with an AI assistant?
Point your assistant at [`llms.txt`](./llms.txt) (a short index) or paste the contents of [`llms-full.txt`](./llms-full.txt) into the chat for full grounded context.
## Configuration
### Single bot account
```json
{
"channels": {
"trueconf": {
"serverUrl": "trueconf.example.com",
"username": "bot_user",
"password": "bot_password",
"useTls": true,
"port": 443
}
}
}
```
`port` is optional — without it the channel uses the default (443 for `useTls:true`, 4309 for `useTls:false`). See the full list of fields in the [reference](#account-field-reference) below.
### Multiple bot accounts
```json
{
"channels": {
"trueconf": {
"accounts": {
"main-office": {
"serverUrl": "trueconf.example.com",
"username": "bot_user",
"password": "bot_password",
"useTls": true
},
"branch-office": {
"serverUrl": "branch.example.com",
"username": "bot_branch",
"password": "bot_branch_password",
"useTls": false,
"port": 5309
}
},
"dmPolicy": "allowlist",
"allowFrom": ["user1@trueconf.example.com", "user2@trueconf.example.com"],
"maxFileSize": 10485760
}
}
}
```
> **Important:** `dmPolicy`, `allowFrom`, and `maxFileSize` live at the `channels.trueconf` level, **not** inside a specific account. If you put them inside `accounts.*`, the channel ignores them and falls back to defaults.
### Account field reference
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `serverUrl` | string | Yes | — | TrueConf Server address (e.g., `10.0.0.1` or `trueconf.example.com`) |
| `username` | string | Yes | — | Bot account username on the server (e.g., `bot_user`) |
| `password` | string \| `{ useEnv: string }` | Yes | — | Bot password. Either a string or an env-var reference: `"password": { "useEnv": "TRUECONF_PASSWORD" }` |
| `useTls` | boolean | Yes | — | `true` — connects via wss/https; `false` — via ws/http. |
| `port` | number | No | `443` when `useTls:true`, `4309` when `useTls:false` | TrueConf Server port (1-65535). |
| `clientId` | string | No | `"chat_bot"` | OAuth client_id. Override only if the server is configured with a non-standard chatbot client |
| `clientSecret` | string | No | `""` | OAuth client_secret. Most TrueConf Server installations use a public client (empty secret) |
| `caPath` | string | No | — | Path to a PEM file with the TrueConf Server certificate. Needed if the server uses a self-signed or corporate CA |
| `tlsVerify` | boolean | No | `true` | When `false`, disables TLS certificate verification **for this TrueConf account only** (per-undici/per-ws). Last-resort insecure mode for self-signed servers; do not use in production. Mutually exclusive with `caPath` — the wizard clears the unused field when you switch modes |
| `setupLocale` | `"en"` \| `"ru"` | No | `en` | Wizard interface language. Set automatically by `trueconf-setup` on first run; you can edit it manually or override via `TRUECONF_SETUP_LOCALE=en\|ru`. Runtime logs always stay English |
| `enabled` | boolean | No | `true` | If false, the account won't run in the gateway but remains in the config |
### Channel field reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `dmPolicy` | string | `"open"` | Direct-message access policy: `"open"` (everyone), `"allowlist"` (only users in `allowFrom`), `"closed"` / `"disabled"` (nobody). `"pairing"` is reserved for future functionality and currently behaves like `"open"` |
| `allowFrom` | string[] | — | List of TrueConf IDs (`user@server`) allowed to DM the bot when `dmPolicy: "allowlist"` |
| `maxFileSize` | number (bytes) | `52428800` (50 MB) | Maximum size of a single file. Applies equally to incoming and outgoing files. Range: 1 byte to 2 GB; on out-of-range values the channel logs `[trueconf] Invalid maxFileSize: ...` and falls back to the default |
| `groupAlwaysRespondIn` | string[] | `[]` | List of titles and/or chatIds whose group messages bypass the mention/reply gate. Formats: ``, `title:`, `chatId:`. See ["Always-respond chats"](#always-respond-chats) above |
### TLS mode
`useTls` picks the protocol
| `useTls` | Protocols | Default port | When to use |
|----------|-----------|--------------|-------------|
| `true` | `wss://` + `https://` | `443` | TrueConf Server with TLS — via Web Manager (443) or a custom TLS port (8443, 9443, ...) |
| `false` | `ws://` + `http://` | `4309` | TrueConf Bridge without TLS — typical for internal networks or behind a reverse proxy |
URLs the plugin builds:
- OAuth token: `{scheme}://{serverUrl}[:{port}]/bridge/api/client/v1/oauth/token`
- WebSocket: `{wsScheme}://{serverUrl}[:{port}]/websocket/chat_bot/`
## Authentication
The plugin uses **OAuth 2.0 Password Grant** to obtain a token and sends the resulting **JWT** in the WebSocket auth packet.
```
1. POST /bridge/api/client/v1/oauth/token
{ "client_id": "chat_bot", "client_secret": "",
"grant_type": "password", "username": "...", "password": "..." }
→ { "access_token": "", "expires_at": 1234567890, ... }
2. WS /websocket/chat_bot/
→ { "type": 1, "method": "auth", "payload": { "token": "", "tokenType": "JWT" } }
```
The token is refreshed automatically a minute before `expires_at` — the user does nothing, reconnects are transparent.
## TLS trust for TrueConf Server
TrueConf Server typically runs on-prem with an internal-CA-signed or self-signed TLS certificate. The channel offers three explicit trust paths, listed in decreasing order of safety:
### 1. System trust (default, no action)
Public certificates (Let's Encrypt, etc.) work out-of-the-box. For an internal CA, make the CA root trusted at the OS or Node level:
- **MDM / GPO / Ansible** — push the CA cert to `/usr/local/share/ca-certificates/` (Linux) or the platform equivalent, then run `update-ca-certificates`.
- **`NODE_EXTRA_CA_CERTS`** — set this env var in your systemd unit, Dockerfile `ENV`, or shell profile:
```bash
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/corp-ca.pem
```
The probe detects `authorized: true` and the wizard skips trust prompts.
### 2. `caPath` — admin-provided root CA (recommended)
If the TrueConf Server administrator gave you a root CA in PEM format, point the channel at it:
- **Wizard interactive**: pick "Specify path to a root CA certificate" on the trust prompt, then enter the path.
- **Manual config**: `"caPath": "/path/to/server-ca.pem"` in `~/.openclaw/openclaw.json`.
- **Headless / CI / Docker / systemd**: `export TRUECONF_CA_PATH=/etc/trueconf/ca.pem` before running `trueconf-setup`.
The wizard validates the file (parses as PEM and verifies it validates the live server) before proceeding. If validation fails, setup aborts with a specific reason.
> On TrueConf Server the certificate is usually stored as `*.crt`. If the file is in PEM format you can rename it to `*.pem`. If you own the server, find the certificate in the TrueConf Server control panel under HTTPS.
### 3. `tlsVerify: false` — disable certificate verification (last resort)
For self-signed servers in safe or restricted environments where obtaining the CA isn't practical:
- **Wizard interactive**: pick "Disable TLS certificate verification for this TrueConf Server" on the trust prompt and confirm the MITM warning.
- **Manual config**: `"tlsVerify": false` in `channels.trueconf` (or per account).
- **Headless**: `export TRUECONF_TLS_VERIFY=false` before running `trueconf-setup`.
This is per-TrueConf only — every other Node HTTPS call (LLM provider, webhooks, etc.) keeps verification on. Setting `tlsVerify:false` and `caPath` together is rejected; `TRUECONF_TLS_VERIFY=false` together with `TRUECONF_CA_PATH` is rejected.
> **Security.** Disabling verification opens TrueConf traffic to a MITM. Use this mode only in a safe or restricted environment (offline lab, trusted internal network).
### Re-running the wizard
On every subsequent run of `trueconf-setup`, the stored trust mode is re-checked against the live server. Three outcomes:
- **Silent happy** — stored `caPath` still validates the server, or `tlsVerify:false` is still in effect → no prompts.
- **Trust anchor mismatch** — stored CA no longer validates (cert rotation or MITM). A banner shows both old + new fingerprints and offers: *Abort / Accept new cert / Use admin-provided file*. **Always verify the new fingerprint with the admin out-of-band before accepting.**
- **Missing trust anchor** — the file recorded in config is gone from disk. Banner offers: *Abort / Re-download chain (legacy) / Use admin-provided file*. If the file was removed by an attacker, re-downloading commits you to whatever the server is presenting — verify first.
### Headless-specific notes
`runHeadlessFinalize` (non-interactive setup invoked with `TRUECONF_SERVER_URL`/`_USERNAME`/`_PASSWORD`) mirrors the trust paths above but all failure modes are fatal — no prompts, no recovery:
- `TRUECONF_CA_PATH=/etc/trueconf/ca.pem` — preferred for production CI/Docker.
- `TRUECONF_TLS_VERIFY=false` — last-resort insecure mode. Mutually exclusive with `TRUECONF_CA_PATH`.
- `TRUECONF_ACCEPT_UNTRUSTED_CA=true` — legacy option that auto-downloads the server's chain into `~/.openclaw/trueconf-ca.pem`. **Use only in trusted networks**; anywhere MITM is possible, use `TRUECONF_CA_PATH` or `TRUECONF_TLS_VERIFY=false` instead.
### Wizard language
The setup wizard defaults to **English**. On first run it prompts you to pick `English` or `Russian`; the choice is saved as `channels.trueconf.setupLocale` so re-runs do not re-prompt. Override via `TRUECONF_SETUP_LOCALE=en|ru` (env wins over config; invalid value fails fast). Runtime logs from the gateway stay English regardless.
## Group Chats
The channel works in group chats. Unlike direct messages, where the bot replies to every message, **in a group the bot only responds when explicitly addressed**:
- **`@` mention**
- **reply to the bot's message** — click "Reply" on one of the bot's recent messages. Buffer: last 50 bot messages per chat.
Messages without a reply or mention are visible to the bot but get no response. The log will contain `[trueconf] group : no mention/reply, dropping`.
**Technical details:**
- Chat type is determined via `getChatByID` on the first message in a new chat and cached until the gateway restarts.
- Each group = one shared session. The bot's conversation history is shared across the group, not per participant. Each message's `senderId` is preserved so the LLM knows who is speaking.
- `dmPolicy` applies only to direct messages. In groups, filtering is based solely on mention/reply.
- TrueConf channels (`chatType=6`) are ignored — the bot does not respond in them. Log: `[trueconf] dropping channel message chatId=`.
### Always-respond chats
By default the bot only replies in groups when @-mentioned or replied to. To make the bot respond to every message in a specific group, list it in `groupAlwaysRespondIn`:
```json
{
"channels": {
"trueconf": {
"groupAlwaysRespondIn": [
"HR-questions",
"title:#devops",
"chatId:51ffe1b1-1515-498e-8501-233116adf9da"
]
}
}
}
```
**Entry formats:**
- `` — the chat title (default). Leading/trailing whitespace and case are normalized; internal whitespace is preserved.
- `title:` — explicitly the title (useful when the title would otherwise look like a chatId).
- `chatId:` — explicitly a chatId. The prefix is required — without it the string is treated as a title.
**Where to find a chatId:** the plugin logs `[trueconf] group ...` on the first message from a chat — copy the `` value.
**Startup behavior:** the plugin queries the bot's full chat list and matches titles. Titles that don't currently match any group are not an error — the bot may not have been added yet, and the bypass activates automatically when it joins.
**Renames:** if a group's title changes, the plugin re-evaluates. A group whose new title is in `groupAlwaysRespondIn` becomes always-respond; one whose new title isn't is removed. The diff is logged.
**Duplicate titles:** if one configured title matches multiple groups, the bypass applies to all of them and the plugin warns. For pinpoint targeting, use `chatId:`.
**Multi-account:** the list is shared across all accounts in the channel. A title `"HR"` matches the same-named group in every account that has one. `chatId` is globally unique on a TrueConf server — if two accounts are members of the same group, the chatId is identical for both.
**Limitations:**
- Only group chats (`chatType=2`) are activated. Channels (`chatType=6`) are not.
- If `getChats` fails on startup, title-based entries stay inactive until the next successful reconnect — `chatId:` entries always work.
- The list is not hot-reloaded; restart the gateway after changes.
## Cross-Server Federation
If multiple accounts are configured (`accounts.office`, `accounts.support`, ...):
- Each account gets its own WebSocket connection, its own CA trust, its own last-inbound-route buffer, and its own recent-bot-messages cache.
- A single account failing to start doesn't affect the others: the gateway logs `[trueconf] Account startup failed: ...` and continues starting the rest.
- Outgoing-response routing uses `ctx.accountId`. The LLM must know which account to reply from; this is typically derived from the inbound `accountId`.
- `dmPolicy`, `allowFrom`, and `maxFileSize` are shared across all accounts (at the `trueconf.*` level). Per-account versions are not supported.
## Media Files and Limits
The channel sends and receives files via TrueConf: images, audio, video, and documents. Size is limited by `maxFileSize` (see the channel field reference). Default — 50 MB.
## Differences from python-trueconf-bot
This channel is wire-compatible with [python-trueconf-bot](https://github.com/trueconf/python-trueconf-bot) — both speak the TrueConf Chatbot Connector protocol. A handful of user-visible behaviors are deliberately different:
| Behavior | This channel | python-trueconf-bot | Why |
|----------|--------------|---------------------|-----|
| Default text rendering | `parseMode: 'markdown'` | `ParseMode.TEXT` | LLMs emit markdown by default; rendering it in TrueConf gives the user formatted output without extra configuration. |
| Long text (> 4096 chars) | Auto-split into chunks (paragraph → sentence → hard cut), order preserved by a per-chat queue | Truncated server-side | LLM responses regularly exceed the limit; paragraph-first splitting is gentler on markdown. |
| Long captions (> 4096 chars) | Sent as a separate message before the file; file is then sent without caption | Same input is truncated server-side | Best-effort: if `sendFile` fails after the caption was delivered, the channel logs the orphan-text condition explicitly. |
| DNS failures (`ENOTFOUND`, `EAI_AGAIN`, …) | 5 retries (≈ 31 s) then fail-fast | Retried indefinitely | Inside an OpenClaw runtime, an unresolvable hostname is almost always a typo in `serverUrl`; surfacing it loudly is more useful than retrying forever. |
| Token expiry (`errorCode: 203`) on any RPC | Transport-level reconnect with fresh OAuth + transparent retry of the original request | User-level pattern via `examples/update_token.py` | Plugins should not require example boilerplate to stay connected. |
| Outbound during WS reconnect or auth failure | Parked in an in-memory queue, drained after the next successful auth | Rejected to the caller; calling code retries | Transient WS drops and token rotations shouldn't lose user-facing replies, and applications shouldn't have to implement their own retry buffer. |
The `setChatMutationHandler` callback (edit / remove / clearHistory events) is not currently exposed; the underlying push events are parsed and logged but not dispatched.
## Environment variables
Six tunables expose the network-resilience knobs that ship with sensible defaults. All are read **at module load** (when the gateway imports the plugin), so they must be set before the gateway starts. Changing them at runtime requires a gateway restart. Invalid values (non-numeric, zero, negative) silently fall back to the default.
| Variable | Default | Unit | Semantics |
|---|---|---|---|
| `TRUECONF_HEARTBEAT_INTERVAL_MS` | `30000` | ms | Interval between WebSocket ping frames sent to TrueConf Server. Lower it for corporate NATs whose idle-connection timeout is below 30s. |
| `TRUECONF_HEARTBEAT_PONG_TIMEOUT_MS` | `10000` | ms | If two pongs in a row don't arrive within this window, the connection is declared dead and reconnected. |
| `TRUECONF_OAUTH_TIMEOUT_MS` | `15000` | ms | Wall-clock budget for the OAuth token POST. Caps the time the lifecycle holds on a hung reverse-proxy before backing off. |
| `TRUECONF_WS_HANDSHAKE_TIMEOUT_MS` | `20000` | ms | Wall-clock budget from `new WebSocket(...)` to the first `'open'` event (TLS + WS upgrade). After this, the socket is terminated and the reconnect loop runs. |
| `TRUECONF_DNS_FAIL_LIMIT` | `5` | count | Cumulative DNS-class failures (`ENOTFOUND`, `EAI_AGAIN`, `EAI_NODATA`, `EAI_NONAME`) during reconnect that trip the `dns_unreachable` terminal path. |
| `TRUECONF_OAUTH_FAIL_LIMIT` | `3` | count | Consecutive 401/403 responses from the OAuth endpoint that trip the `oauth_unauthorized` terminal path. Counter resets on any non-401/403 outcome. |
## Troubleshooting
### `fetch failed` on startup
- **Cause:** The TrueConf Server certificate is not from a public CA (self-signed or from a corporate CA), and Node.js doesn't trust it.
- **Solution:** Re-run `trueconf-setup`. Pick "Specify path to a root CA certificate" and provide a `.pem` from the server admin (preferred), or "Disable TLS certificate verification for this TrueConf Server" if you accept the MITM risk for this transport (last resort). You can also set `"caPath": "/path/to/ca.pem"` or `"tlsVerify": false` in the config manually, or export `NODE_EXTRA_CA_CERTS=/path/to/ca.pem` for system-wide trust. See [TLS trust for TrueConf Server](#tls-trust-for-trueconf-server).
- **What not to do:** `NODE_TLS_REJECT_UNAUTHORIZED=0` disables TLS validation for the **entire** Node process, including calls to the LLM provider and any other endpoints. Use the per-account `tlsVerify: false` option above if you need an insecure mode — it scopes the bypass to TrueConf only.
### `blocked URL fetch ... resolves to private/internal/special-use IP address`
- **Cause:** A corporate proxy or VPN redirects LLM-provider requests (e.g., api.openai.com) to an internal address.
- **Solution:** Configure a proxy (`HTTPS_PROXY`) or use a local model via Ollama.
### `Missing API key for provider "openai"`
- **Cause:** LLM provider is not configured.
- **Solution:** Run `openclaw configure` and pick a provider (OpenAI, Ollama, etc.).
### Invalid credentials
- **Symptom:** `OAuth token acquisition failed (401): invalid_grant` in the logs.
- **Cause:** Wrong `username` or `password` in the config.
- **Solution:** Make sure `username` is just the account name (`bot_user`), without `@server.trueconf.name`; the server address goes separately in `serverUrl`. Verify the account is active in the TrueConf Server admin panel. If the server is configured with a non-standard OAuth client, set `clientId` and `clientSecret` in the config.
### `openclaw plugins install` blocked by security scanner
- **Symptom:** `openclaw plugins install @trueconf-community/trueconf-openclaw-channel` aborts with a critical scanner finding citing `Environment variable access combined with network send` in `bin/trueconf-setup.mjs`, `src/channel-setup.ts`, or `src/ws-client.ts`.
- **Cause:** The openclaw plugin scanner uses a per-file regex that flags any source file containing both `process.env` and one of `fetch` / `post` / `http.request`. Versions of this plugin earlier than the scanner-friendly refactor inlined the env reads in those files; the current release moved them to `src/env-config.ts` so the scanner regex no longer matches.
- **Solution:** Upgrade to the latest published `@trueconf-community/trueconf-openclaw-channel` — the scanner-friendly refactor is shipped. If you are pinned to an older version (or your openclaw build uses a customized scanner ruleset that still flags the current release), the fallback is to install with the dangerous-flag override:
```bash
openclaw plugins install --dangerously-force-unsafe-install @trueconf-community/trueconf-openclaw-channel
```
- **What not to do:** Do not adopt `--dangerously-force-unsafe-install` as a default install command. The flag bypasses ALL plugin-security checks, not just the env-harvesting rule, for ALL plugins it installs. Use the upgraded plugin first; reach for the flag only as a last-resort compatibility shim.
### `unknown channel id: trueconf` at `openclaw gateway`
- **Symptom:** the gateway refuses to start with `Invalid config ... channels.trueconf: unknown channel id: trueconf` — often after it previously worked ("worked yesterday, broke today").
- **Cause:** `trueconf-setup` was run via `npx -p ... trueconf-setup` **without** a prior `openclaw plugins install`. The wizard then recorded the disposable npx cache dir (`~/.npm/_npx//...`) as the plugin load path; once npm evicts that cache, the channel can no longer be found.
- **Solution:** remove the stale `~/.npm/_npx/...` entry from `plugins.load.paths` in `~/.openclaw/openclaw.json` (or run `openclaw doctor --fix` if your openclaw build supports it), then reinstall and re-run setup:
```bash
openclaw plugins install @trueconf-community/trueconf-openclaw-channel
npx -y -p @trueconf-community/trueconf-openclaw-channel trueconf-setup
openclaw gateway
```
(Or run `openclaw onboard` and pick TrueConf instead of the `npx ... trueconf-setup` step.) Always run `openclaw plugins install` **before** `trueconf-setup`. Recent versions fail fast with this guidance instead of writing the bad path.
### Channel gone right after running `trueconf-setup` (openclaw 2026.6.x)
- **Symptom:** the plugin was installed and loading fine, but after a successful `trueconf-setup` run the gateway silently skips it — no `[trueconf]` lines in the log, the channel is missing from `openclaw channels status`.
- **Cause:** plugin versions before 1.2.9 removed `plugins.entries.trueconf` from `openclaw.json` as a "stale" entry (1.2.8 fixed this for tarball installs but missed registry installs, which live under `~/.openclaw/npm/projects/`). On openclaw 2026.6.x that entry is the live enable record for an installed plugin, so deleting it disabled the plugin.
- **Solution:** upgrade the plugin to >= 1.2.9 (the cleanup now only runs when no install evidence exists), then restore the entry — either re-run `openclaw plugins install @trueconf-community/trueconf-openclaw-channel`, or add it back manually in `~/.openclaw/openclaw.json`:
```json
{ "plugins": { "entries": { "trueconf": { "enabled": true } } } }
```
### TLS mismatch
- **Symptom:** `WebSocket error: connect ECONNREFUSED` right after start.
- **Cause:** `useTls` doesn't match the server's configuration, or the `port` is wrong.
- **Solution:** Make sure `useTls` matches the server's protocol (TLS ↔ true). If the port is non-default, set `port` explicitly. See [TLS mode](#tls-mode).
### Port blocked by firewall
- **Symptom:** `WebSocket error: connect ECONNREFUSED` or `ETIMEDOUT` with otherwise-valid `serverUrl`/`useTls`.
- **Cause:** The firewall blocks the chosen port (4309, 443, or a custom one).
- **Solution:** Open the port on the firewall, or switch to a different port/mode (Web Manager on 443 with `useTls:true`, Bridge on 4309 with `useTls:false`, a custom port via the `port` field).
### Bot connects but doesn't respond
- **Symptom:** The log shows `Connected and authenticated`, but there are no replies.
- **Possible causes:**
1. You're messaging the bot from the same account — you need to write from a **different** user.
2. Group-chat message without an @-mention and without a reply to the bot — in a group you have to address the bot explicitly (see the [Group Chats](#group-chats) section). The log will contain `group : no mention/reply, dropping`.
3. Message in a TrueConf channel (`chatType=6`) — the bot ignores them. Log: `dropping channel message chatId=`.
4. `dmPolicy: "allowlist"` and the sender isn't in `allowFrom`. Log: `DM blocked for by policy`.
5. LLM provider is not configured — run `openclaw configure`.
6. LLM provider is unreachable from the network — check access or use Ollama.
### Bot sends a photo without the caption
- **Symptom:** The agent replies "Here's a cat 🐱" with an attached picture; the TrueConf chat only shows the photo, the text is gone.
- **Cause:** In openclaw `2026.4.22`, when `agents.defaults.blockStreamingDefault` is `"off"` (also the default when the field is unset), the caption and the media are split into separate blocks and the text part is dropped by the final filter before reaching the plugin's deliver.
- **Solution:**
```bash
openclaw config set agents.defaults.blockStreamingDefault on
openclaw config set agents.defaults.blockStreamingBreak message_end
```
Then restart the gateway. The agent will hand text and media to the plugin as a single block, and the plugin will send the photo with the caption.
### Frequent reconnects
- **Symptom:** `[trueconf] Connection closed, scheduling reconnect` repeats in the logs.
- **Cause:** WebSocket connection drop at the network layer. Heartbeat runs as ping/pong on a 20-second interval — if two pongs in a row don't arrive within 20 seconds, the connection is considered dead and the plugin reconnects.
- **Solution:** Check network stability to TrueConf Server, traceroute, MTU, corporate proxy/VPN between the client and the server. If there's a reverse proxy (nginx, haproxy) between the plugin and the server, make sure the WebSocket idle-connection timeouts there are large enough.
- **Outbound during reconnect:** messages sent while the WS is down are parked in memory and delivered after the next successful auth — replies aren't lost across reconnects, only delayed. Trace a single request via `[trueconf] outbound qid=` lines stepping through `submit → wait_auth → wire_send → ack`.
### Finding the logs
All plugin messages are prefixed with `[trueconf]`. To filter:
**Linux/macOS:**
```bash
openclaw gateway 2>&1 | grep '\[trueconf\]'
```
**Windows (PowerShell):**
```powershell
openclaw gateway | Select-String '\[trueconf\]'
```
Log file: the path is shown in the gateway output (`[gateway] log file: ...`).
## Testing
```bash
npm test
```
## License
MIT
# === Source: CHANGELOG.md ===
# Changelog
## [1.2.9] - 2026-06-10
### Fixed
- The npx-cache gate and the entries cleanup now recognize **registry** installs on openclaw >= 2026.6.x. `openclaw plugins install ` on those versions stores the package under `/npm/projects/trueconf-community-trueconf-openclaw-channel-/` (not `extensions/trueconf`, which 1.2.8 checked) and leaves only `plugins.entries.trueconf` in the raw config. In 1.2.8 the documented `openclaw plugins install` -> `npx ... trueconf-setup` order therefore still dead-ended with "plugin is not installed", and a setup run could still delete the live `plugins.entries.trueconf`, silently disabling the plugin.
- Install detection now accepts `plugins.entries.trueconf` itself as evidence (every observed openclaw version writes it on install). A stale entry letting the gate pass is the cheap failure: the load-path registrar refuses npx-cache dirs, so a false pass can no longer poison the config — while a false block dead-ends the documented flow. The stale-entries cleanup keeps its narrower disk-evidence check (it would otherwise be circular) and still removes a leftover entry when no plugin bits exist anywhere.
## [1.2.8] - 2026-06-10
### Fixed
- `trueconf-setup` now fails fast instead of recording a disposable npx cache path (`~/.npm/_npx//...`) in `openclaw.json` when the wizard is run via `npx -p` without a prior `openclaw plugins install`. Prevents the channel later failing to load with `unknown channel id: trueconf` once the npx cache is evicted.
- The npx-cache gate detects an installed plugin on openclaw >= 2026.6.x too: those versions keep install records in the plugin index instead of `plugins.installs` inside `openclaw.json`, so the gate now also accepts an `extensions/trueconf` directory next to the config as install evidence. Without this, the documented `openclaw plugins install` -> `npx ... trueconf-setup` order dead-ended with "plugin is not installed" on a machine where it was.
- `trueconf-setup` no longer deletes `plugins.entries.trueconf` when the plugin is installed. On openclaw 2026.6.x that entry is the live enable record — removing it made the gateway silently skip the installed plugin right after a successful setup (8-plugins-instead-of-9 startup, no `[trueconf]` log lines, channel gone). The stale-entry cleanup still runs when no install evidence exists.
- `trueconf-setup` no longer appends a redundant `plugins.load.paths` entry when the plugin is already installed (extensions/install-record discovery already wires loading).
## [1.2.7] - 2026-06-02
### Fixed
- Account startup no longer fails with `OAuth token request failed: fetch failed (UND_ERR_INVALID_ARG: invalid onRequestStart method)` on hosts whose runtime `undici` differs from the one this plugin bundles. The plugin ships its own `undici` and called `fetch`/`request` without an explicit dispatcher, so those calls fell through to the process-global dispatcher. The OpenClaw runtime installs a custom global dispatcher built from *its own* `undici` (stream-timeout / env-proxy wrapping via `setGlobalDispatcher`); dispatching a request handler created by our bundled `undici` through that foreign Agent throws `UND_ERR_INVALID_ARG`. This reproduced on a host running Node 24+ (whose built-in undici is a newer major than the bundled `^7`) with a private-network LLM provider configured — the gateway aborted the channel at `Account default startup failed`. The OAuth token request (`ws-client`), inbound media download (`inbound`), and outbound media fetch (`load-media`) now always dispatch through an `Agent` from the plugin's own `undici`, so they never route through the host's global dispatcher. The outbound file upload (`outbound`) already used the runtime's global `fetch` and was unaffected. Verified red→green against a live gateway on Node 26.
### Changed
- New internal `src/default-dispatcher.ts` exposes a lazily-created, pooled `Agent` (from the plugin's own `undici`) reused as the default dispatcher across the OAuth, inbound-download, and media-fetch paths. The existing `global-fetch-isolation` regression now also covers a broken global dispatcher (not just a broken global `fetch`).
## [1.2.6] - 2026-06-02
### Fixed
- The published npm package now ships compiled JavaScript so it installs into OpenClaw. 1.2.5 (and earlier) shipped only TypeScript source (`main: ./index.ts`, no `dist/`), so `openclaw plugins install @trueconf-community/trueconf-openclaw-channel` failed with `requires compiled runtime output for TypeScript entry ./index.ts` (`TypeScript source fallback is only supported for source checkouts`). Installing from npm now works without a manual source build.
### Changed
- Build pipeline: a `build` step (`tsc` + `tsc-alias` + copy of `src/probe.mjs`) compiles the plugin per-file into `dist/`, runs in CI before publish, and `dist/` is now in the published `files` with `main`/`extensions`/`setupEntry` pointing at it. Per-file output (not a single bundle) preserves the env/network file separation so the OpenClaw install scanner stays green. Also dropped a stale `tsconfig` `include` entry for a non-existent root `setup-entry.ts`.
## [1.2.5] - 2026-06-01
### Added
- Global bot nicknames (#29). The agent can be taught names it answers to in group chats: a group message now activates the bot on @-mention, reply, **or** any configured nickname. Three agent tools — `remember_bot_nickname`, `forget_bot_nickname`, `list_bot_nicknames` — manage a global, disk-backed nickname list that survives Gateway restarts. Tool names are declared in `openclaw.plugin.json` under `contracts.tools` so the gateway registers them.
- Quoted-message reply context (#29). When an inbound message quotes/replies to another message, the plugin fetches the quoted message via `getMessageById` and prepends its author and text as context for the agent, so a reply to a quote is understood without the surrounding history.
### Fixed
- Account startup no longer leaks resources (#30). When `lifecycle.start()` throws, the account's undici dispatcher (keep-alive socket pool) and registry entry are now torn down (`shutdownAccountEntry` + `accounts.delete` + `clearAccountChats`) instead of lingering as a zombie that could shadow a later re-add.
- Inbound dispatch no longer produces an unhandled promise rejection (#30). A throwing async `onInboundMessage` handler is now caught and logged, matching the existing push-listener guard.
- `forget_bot_nickname` reports the truth when the on-disk write fails (#30). `NicknameStore.remove()` returns a discriminated result (`removed` / `not_found` / `persist_failed`) instead of always claiming success, so a nickname can no longer silently reappear after a restart.
- The nickname store reloads from disk when the file changes (#29), so a nickname registered through a tool is seen by the in-process activation gate without a Gateway restart.
### Changed
- The two markdown sanitizers now share a single `sanitizeMarkdownCore` helper (#30); the differing blank-line collapse step is injected. Output is unchanged.
### Removed
- Dead code (#30): `buildAck`, `IdCounter.current()`, `RequestMatcher.size`, the write-only `pluginRuntimeStore`, and the unused probe helpers `decide()` / `categorizeOAuthError()` (production routing already lives in `probeTls` / `validateOAuthCredentials`).
## [1.2.4] - 2026-05-03
### Added
- Six ENV-tunable knobs for corporate-NAT and slow-network operators (read once at module load — change requires Gateway restart):
- `TRUECONF_HEARTBEAT_INTERVAL_MS` (default 30000) and `TRUECONF_HEARTBEAT_PONG_TIMEOUT_MS` (default 10000) — operators behind sub-30s idle-timeout NATs can lower the ping cadence without forking.
- `TRUECONF_OAUTH_TIMEOUT_MS` (default 15000) — `AbortSignal.timeout` cap on the OAuth POST so a hung reverse-proxy cannot pin the lifecycle indefinitely.
- `TRUECONF_WS_HANDSHAKE_TIMEOUT_MS` (default 20000) — wall-clock cap from `new WebSocket(...)` to the first `'open'` event; on timeout, `ws.terminate()` releases the socket and the existing reconnect loop runs.
- `TRUECONF_DNS_FAIL_LIMIT` (default 5) and `TRUECONF_OAUTH_FAIL_LIMIT` (default 3) — bound the cumulative DNS retry count and the consecutive OAuth 401/403 count before the lifecycle gives up.
- OAuth 401/403 terminal lifecycle path. After `TRUECONF_OAUTH_FAIL_LIMIT` consecutive 401 or 403 responses, the lifecycle emits `onTerminalFailure({ kind: 'auth_exhausted', retries: N, cause: NetworkError })`, logs `[trueconf] OAuth authentication failed N times; check bot credentials (username/password) on TrueConf Server. Giving up.`, rejects the auth barrier, and stops scheduling reconnects. Recovery is restart-only in v1.2.4. `OAUTH_TERMINAL_CODE = 'OAUTH_GIVEUP'` and `isAuthTerminalCode(401|403)` exported from `types.ts`.
- At-least-once outbound delivery via in-memory `OutboundQueue`. Outbound requests (`sendMessage`, `sendFile`, `uploadFile`, `createP2PChat`) survive arbitrary-length WS reconnect windows by parking on transport errors and draining on each successful auth. Closes failure-mode classes (i)/(iii)/(v)/(vi)/(vii) from item 49.
- `LifecycleOptions.onTerminalFailure` callback fires from `lifecycle.shutdown()`, after DNS retry exhaustion (`kind: 'dns_exhausted'`), and after OAuth 401/403 exhaustion (`kind: 'auth_exhausted'`). Wired to `outboundQueue.failAll` in `channel.ts` so terminal failures reject pending outbound with explicit cause; the discriminated-union design keeps this wiring byte-identical across new variants.
- Outbound lifecycle instrumentation (`submit` / `wait_auth` / `wire_send` / `ack`) correlated by `qid` for failure-mode diagnosis (L1c). Each outbound request emits four `.info` log lines threading a single id from `OutboundQueue.submit` through `WsClient.sendRequest` and `sendRequestInternal`. Direct ws-client callers (`subscribeFileProgress`) skip the lifecycle log via the optional `traceId` parameter.
### Fixed
- Handshake-timer scope was previously bleeding into the auth round-trip — a slow auth response could trigger `WS_HANDSHAKE_TIMEOUT` after the upgrade had already completed. Timer now clears on `ws.on('open')` per the documented handshake-only contract.
- Synchronous re-entry guard on `ConnectionLifecycle.start()` via a `startInFlight` flag prevents the boot path from racing the close-handler-driven `scheduleReconnect()` when a handshake-timeout `ws.terminate()` re-enters `handleClose` while the bootstrap caller is still receiving the rejection.
- WS error metadata extraction no longer produces the literal string `'undefined'` for `code`/`syscall`/`hostname` when the property exists but is undefined — switched to `=== undefined` guards matching the existing `extractFetchCauseMeta` idiom.
- `acquireToken` non-ok response branch now throws `NetworkError(phase='oauth', code=String(status))` instead of generic `Error`, so the new `isAuthTerminalError` classifier in `scheduleReconnect` can distinguish 401/403 (terminal) from 500/502 (transient).
- Reconnect-time WS-handshake and auth-response failures (e.g. `ECONNREFUSED`, `Server sent no subprotocol`, `Auth failed: errorCode N`) now park outbound items instead of rejecting them. `lifecycle.start` wraps the auth-barrier rejection as parkable so `waitAuthenticated` callers go through the queue's park-and-drain branch; terminal causes (DNS exhaustion, OAuth 401/403 exhaustion, shutdown) keep their non-parkable form so `onTerminalFailure -> failAll` still flushes pending items on give-up.
### Changed
- `DNS_MAX_RETRIES` static class constant hoisted to module-level `DNS_FAIL_LIMIT` sourced via the same `readEnvMs` helper as the timing constants. Behaviour unchanged at the default (5 cumulative DNS-class failures); tunable via `TRUECONF_DNS_FAIL_LIMIT`.
- DNS giveup log message phrasing aligned with the post-increment counter — `gave up after N attempts` (was `N retries`, which was off-by-one against the configured limit).
- `outbound.ts` migrated from `wsClient.sendRequest` / `sendRequestWithReconnectRetry` to `outboundQueue.submit` for all 5 outbound sites. Helper signatures (`sendText`, `sendTextToChat`, `createP2PChat`, `resolveDirectChat`, `recreateChat`) cascade `outboundQueue` parameter.
### Removed
- `outbound.ts:sendRequestWithReconnectRetry` and `DISCONNECTED_RETRY_DELAYS_MS` — superseded by `OutboundQueue` (longer parking window, no fixed retry budget).
## 1.2.2
Fixed plugin failing to load against current openclaw versions due to
reliance on a private SDK subpath (`openclaw/plugin-sdk/mattermost`)
for `loadOutboundMediaFromUrl`. Internalized the helper in
`src/load-media.ts` to remove the dependency on non-public openclaw
internals.