Connect a chat app to Virgo
Virgo's team can answer where people already talk. A chat adapter connects one chat provider — Discord, Microsoft Teams or Telegram — to a Virgo installation you already run, so a person can address a Seat from their own chat client and receive the reply in the same conversation, thread and message.
This page explains what a chat adapter is, what it needs, and exactly how far the current release will take you. The three provider guides then walk through one provider each.
Two different things are called an adapter
An agent-provider adapter — Claude or Codex — attaches a Seat to the provider conversation where an agent actually works. A human chat adapter — Discord, Teams, Telegram — brings people in: users, channels, threads and replies.
Both terminate at the Hub, and that is the point. The adapter owns its provider's mechanics: gateways, webhooks, polling, attachment transfer, reaction state. The Hub keeps what must not vary by provider: who a sender is, what they may ask for, which Seat receives it, and the durable record of what was delivered.
One installation, not one bot per Seat
A chat adapter is not a separate product to install beside Virgo, and it does not add a bot for each Seat. One configured bot serves the destinations you allow, and the Hub decides which Seat receives each accepted message. Provider display names and usernames are diagnostic only; they never select a policy or grant access.
Adding one to an installation you already have
This used to be the gap on this page. It is now a supported route: adapter add, adapter list and adapter remove change the adapter roster of an installation that already exists, without replacing it.
adapter add --file /absolute/path/to/descriptor.json
adapter list
adapter remove <id>
The settings arrive as a file, and the path must be absolute. They are the provider module's own input to decode, so there is no flag-per-setting surface here.
adapter add refuses more than it accepts, which is the point. A descriptor must name an id, and an id already in the roster is a conflict rather than an update — replacing would silently drop whatever the old entry carried. Only the new entry is prepared, so a neighbouring adapter's stored output is never rewritten. The whole roster is then validated the way a boot validates it, including unique ids, sessions and Seats, so an add cannot leave behind a configuration that will not start.
A credential is registered in the same operation. The descriptor may carry a credentialBinding naming a reference and its source; the reference is written into the installation's settings together with the adapter, not as a second step that could half-succeed. Pointing an existing reference at a different source is refused, because that would silently redirect whatever already reads it. So is a binding the module's own settings never name, because nothing would read it.
adapter list reads back what is registered: each entry's id, kind, session, Seat and chat target, the credential reference it reads, and the module's own complaint if the entry is invalid. It names the reference only — never where the secret lives and never its bytes.
adapter remove takes the id, and drops a credential reference nothing left in the installation still names.
A new adapter becomes active on the next Host start. The running Host holds the adapters it booted with; there is no reload. Native agent sessions are deliberately left alone, because restarting them to add a chat would disturb exactly the sessions this is meant to preserve.
Do not reinstall a Hub to add a chat. That was true when there was no route and it is still true now that there is one: reinstalling replaces a running installation to gain a configuration entry that adapter add writes in place.
Who is allowed to talk to the team
Every authenticated provider event passes a shared access gate before anything reaches the Hub. Three modes:
| Mode | Meaning |
|---|---|
open | Any sender on the configured, authenticated channel is admitted. |
restricted | Only senders on an explicit allowlist are admitted. |
approval | A new sender's first message is retained and an administrator decides. |
Defaults differ by provider. Discord and Telegram both default to approval; Teams defaults to open inside its authenticated tenant-and-conversation binding.
A provider that defaults to approval needs two things in its descriptor or installation will refuse it: at least one administrator principal, and a route to announce pending requests on — accessNotificationRoute for Discord, accessNotificationChatId for Telegram.
An administrator decides a pending request with an exact reserved message:
/virgo-access approve <requestId>
/virgo-access deny <requestId>
Teams administrators may instead submit the typed card data { "kind": "virgo.channel_access.decision.v1", "decision": "approve" | "deny", "requestId": "..." }. These reserved messages are intercepted before ordinary access evaluation and are never forwarded to an agent. The command itself grants nothing: the sender is checked against the configured administrator list for that exact provider, channel and tenant. A malformed command or an unauthorised decision is a terminal denial.
Leaving approval mode revokes every existing grant on that channel at once. This version has no per-principal revocation while remaining in approval mode.
Choosing where messages go
Each descriptor names a targetVsp — the canonical Virgo destination for accepted inbound messages — and a default outbound conversation. It also bounds which conversations are in scope at all: allowed guild and channel IDs on Discord, an exact tenant and conversation on Teams, allowed chat IDs on Telegram.
The return address is immutable and recorded per message, so an agent's reply goes back to the conversation, thread and message that asked.
What each provider can do
| Discord | Microsoft Teams | Telegram | |
|---|---|---|---|
| Transport | Gateway v10 and Bot API v10 | Bot Connector REST on your HTTPS endpoint | Bot API getUpdates long polling |
| Default access | approval | open within the authenticated binding | approval |
| Text limit | provider limits | 32,000 UTF-8 bytes | 4,096 Unicode code points |
| Rich cards | not used | Adaptive 1.4, Hero, Thumbnail | not supported |
| Reactions | add and remove its own | pinned catalog of exact IDs, plus documented aliases | emoji reactions |
| Files inbound | attachments, within configured ceilings | personal chat only, with a managed file port | documents and photos, with an artifact port |
| Files outbound | attachments, within configured ceilings | personal chat only, via Teams file consent | documents, with an artifact port |
| Resend safety | — | no effect-key idempotency; an ambiguous result stays unknown | no effect-key idempotency; an ambiguous result stays unknown |
| Exclusivity | — | — | one active poll owner per bot token |
Teams files are personal chat only, in both directions. Sending uses Teams file consent. Receiving works on the same personal-chat attachments: with supportsFiles enabled and a managed file port supplying storeInbound, the adapter downloads and stores the authenticated attachment Teams sends, and without that port it refuses the activity rather than dropping the file silently. Group and channel file transfer needs Microsoft Graph and user authorisation, and is not implemented here in either direction.
The shared conversation commands are not live yet
/setseat, /listseat and an explicit ~seat recipient are designed and have source, and the provider guides describe them because they are part of the intended conversation model. They are not something to expect from an installed chat today: the source ledger records that the shared Hub conversation is not composed into installed chat ingress. Treat them as design, not as available behaviour.
Next
Each guide takes the same shape: what the provider needs, the minimum permissions, where the credential lives, the descriptor you will need, the access decisions, how to register it on an installation you already have, and the verification chain to run afterwards.
Registration itself has been exercised on a canonical route for Discord, where the adapter loaded into an existing installation and the Host reached Gateway READY. Acceptance stops there. An agent claiming its Seat over a chat adapter, a human message arriving inbound, and a correlated reply going back out are not accepted on any provider yet.