Browse docsChat connections

Connecting a chat account

Pairing connects one chat account — a Slack app or a Telegram bot — to your Enso home and records you as its owner. enso setup guides a fresh local home through provider choices and that private chat pairing. A host such as Enso Cloud drives the same pairing over JSON with enso connect. Neither flow logs into a provider account or starts a provider during pairing. Provider subscription login remains manual; see Install.

Pairing

You create the bot yourself in Slack or Telegram; Enso then waits for one challenged private message from you and keeps the identity it learns. The default installation includes both transports. If you installed with --extras and left one out, pairing stops and tells you to install enso[slack] or enso[telegram]; see Installation.

Each attempt waits for at most five minutes, then closes; a closed attempt cannot be resumed, so start a new one to get a fresh code. Success sends one connection acknowledgment before closing; it does not dispatch the pairing message to an AI provider. An acknowledgment failure is a pairing failure. serve, native setup, and hosted pairing share a per-home receive lock, so only one of them can receive chat messages at a time. The normal service must stop before a new connection or changed hosted defaults are applied.

Slack

Slack needs two tokens from the same app: the Bot User OAuth Token (xoxb-…) and an App-Level Token (xapp-…) with connections:write.

  1. Print the bundled app definition with enso slack manifest. Preparing a home also leaves a copy at ~/.enso/slack/manifest.json.
  2. At https://api.slack.com/apps?new_app=1, choose From an app manifest, pick your workspace, and paste that JSON. It enables Socket Mode and requests exactly the scopes and events Enso uses.
  3. Install the app to the workspace, approve the permissions, and copy its Bot User OAuth Token.
  4. In Basic Information → App-Level Tokens, create a token with connections:write and copy it.

Setup checks bot identity, opens Socket Mode with the app token, and checks that both tokens belong to the same app. It then gives the user a private-chat link and a fresh ENSO-… code. Only a new direct message whose whole text is that code, sent from a human account, can pair the owner. Thread replies, bot messages, and events from another workspace do not pair an account. The resulting slack:dm:USER binding and notification target use that owner’s private conversation.

Telegram

Telegram needs the bot token created by BotFather with /newbot: send /newbot, answer its naming prompts, and copy the token it prints. Use a new bot, so no other service is already receiving its updates.

Setup verifies bot identity and refuses an existing webhook. A fresh t.me Start link carries a random challenge; open it and tap Start in the bot’s private chat. Only the matching new private /start from a human account can pair the owner. Old, forwarded, group, and unrelated messages are ignored. Setup records the owner in allowed_users, binds their private chat, and uses it for notifications. A Telegram 409 stops pairing and reports the conflict, so the user can stop the competing receiver or create a new bot. Setup never deletes a webhook or takes over another polling process automatically.

What pairing writes

Pairing fills in the chat part of config.json:

Key What pairing sets
transports.slack bot_token, app_token, and notify — the owner’s private conversation
transports.telegram bot_token, allowed_users holding the owner’s numeric id, and notify
bindings slack:dm:U… or telegram:<user id> mapped to the default workspace

The same write also records your chosen provider, model, and effort. More channels, more workspaces, and extra bindings are ordinary configuration you edit afterwards; see Transports and Bindings. A home that already has config.json cannot be paired again; change an existing connection there instead.

Hosted command contract

enso connect start --transport slack|telegram --file FILE|- [--json]
enso connect status [ATTEMPT_ID] [--json]
enso connect cancel [ATTEMPT_ID] [--json]
enso connect finish --file FILE|- [--json]

start initializes the home if needed; a new attempt requires an absent active config. Its input is one JSON object, at most 16,384 characters, containing request_id, bot_token, and for Slack app_token. The request ID is 16–128 letters, digits, underscores, or hyphens. Supply secrets through stdin or a private file, never command arguments. Retrying the latest request ID with the same transport and credentials returns its existing attempt, even after apply; changed credentials or a fresh attempt require a new ID. Only the latest attempt is retained. A second receiver is refused until the first closes or is cancelled.

enso connect start --transport slack --file - --json <<'JSON'
{
  "request_id": "setup-2026-09-15-0001",
  "bot_token": "xoxb-your-bot-token",
  "app_token": "xapp-your-app-token"
}
JSON

Each credential must be 8–4096 characters with no spaces. Telegram sends only request_id and bot_token; sending it an app_token value is rejected. Unknown keys are rejected for both transports.

status and cancel without an ID operate on the latest attempt. Supplying a different ID fails instead of affecting a newer attempt. The ID is a positional argument, for example enso connect status ATTEMPT_ID --json, not an --attempt-id option. Cancellation invalidates the challenge, closes the receiver, and removes temporary credentials. An interrupted process is reconciled on the next status read. Cancel never removes or changes an applied config.

finish accepts exactly attempt_id, defaults (the provider, model, effort triple), and expected_hash. The agent must be a bundled model and an effort that its adapter accepts without clamping; use enso providers --json for offline choices, not as an authentication check. For the first apply use "missing". It constructs the complete config on the local machine from the paired identity and private credentials, then uses validated, atomic configuration apply. A repeated successful apply is idempotent, including a lost command response. Once applied, the same attempt can revise its AI defaults using the current hash; other config fields are preserved. A config created outside this pairing flow is preserved and refused. Changing transports requires a fresh home; existing connections retain the ordinary manual configuration workflow.

enso connect finish --file - --json <<'JSON'
{
  "attempt_id": "3f0b0f1c-6b0c-4a1f-9f5e-1c2d3e4f5a6b",
  "defaults": {"provider": "claude", "model": "opus", "effort": "high"},
  "expected_hash": "missing"
}
JSON

All four commands with --json emit one result and exit 0 on success:

{
  "version": 1,
  "ok": true,
  "connection": {
    "attempt_id": "…", "request_id": "…", "transport": "slack", "state": "waiting",
    "bot_name": "enso", "workspace_name": "My workspace",
    "open_url": "https://slack.com/app_redirect?…", "instruction": "ENSO-…",
    "expires_at": "2026-09-09T20:00:00+00:00"
  },
  "config_valid": false,
  "config_hash": "missing",
  "first_reply": null
}

connection is null before an attempt. Fields not yet known are omitted. States are verifying, waiting, paired, applied, failed, cancelled, and expired. paired is exposed only after the receiver releases its connection. Terminal failures add error and error_code; applied state adds defaults. config_valid means local validation passed, not that a provider can respond. config_hash identifies the current exact config. A successful status command can still report a failed or expired connection: consumers must inspect connection.state, not only the top-level ok.

Command failures exit 1 with {"version":1,"ok":false,"error":"…","error_code":"…"}. Codes distinguish invalid_input, already_configured, busy, conflict, missing_extra, invalid_token, missing_scope, mismatched_app, webhook_conflict, polling_conflict, connection_failed, cancelled, expired, not_paired, not_found, config_invalid, and storage_error. Messages are safe for display and never contain a third-party response or submitted bot credentials. Invalid CLI syntax still exits 2.

Without --json the same commands print ATTEMPT_ID: state followed by the open link, the pairing code, or the error, or No connection attempt yet. before the first attempt; a failure prints only its message and exits 1.

Private state and completion

Hosted pairing stores its pending credentials in cache/connect/credentials.json with owner-only file access, inside an owner-only directory. The attempt state, advisory locks, and optional reply receipt live alongside it. Pending credentials remain while a paired owner finishes setup; failure, expiry, cancellation, or successful apply removes that temporary file. The applied tokens remain literal values in the private config.json. Native setup keeps credentials in memory until its config write.

First-reply receipts belong to the hosted connect flow; native setup does not create a hosted attempt or receipt. After manual provider login and service start, the user sends a normal message. Runtime records a receipt only after delivering a successful answer using the selected provider, model and effort to the paired owner and conversation. Empty responses, provider errors, failed formatting repairs, timeouts, cancellations, and failed delivery do not qualify. The receipt contains the attempt ID, agent triple, transport, owner, conversation, timestamp, and config hash; it contains no prompt or response text. It matches the configuration snapshot the answering turn ran with — each turn reads config.json fresh — and the hash saved by connect finish. A config change hides old completion evidence. Reconcile changed configuration with connect finish using the current hash, then get a successful reply from that configuration before expecting a new receipt; the next turn already runs under the changed file, but changing the file alone does not update the pairing attempt’s saved hash.