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.
- Print the bundled app definition with
enso slack manifest. Preparing a home also leaves a copy at~/.enso/slack/manifest.json. - 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.
- Install the app to the workspace, approve the permissions, and copy its Bot User OAuth Token.
- In Basic Information → App-Level Tokens, create a token with
connections:writeand 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.
