Concepts
How a bot gets into a server, what it can see once there, and what Hexis does and does not promise about it.
Install and the managed role
- Who can install. Anyone with Manage Server in the target server, for their own apps. An app that is not approved can only be installed by its owner.
- Where it happens. The installing app signs a grant for the bot’s device, and only the Hexis client holds that key, so the last step always runs in a client. Today that is the
/bot installcommand in the command-line client (see the quickstart). The public link (accounts.hexis.chat/oauth/authorize?client_id=hxa_…&scope=bot&permissions=…&kind=reader|noread) and the server picker in the apps arriving (#1429) will hand off to the same step. The install call is refused for everyone, in every server, until the server’s minimum client version reaches the first build that understands bots (default 0.3.2), and the operator arms that floor arriving (#1476). - The managed role. Installing creates a role named after the bot, holding the permission bits the installer chose. It sits just below the installer’s top role. It cannot be given to a person, cannot be deleted by hand (removing the bot deletes it), and cannot be moved to or above the mover’s own top role. Its bits can be edited later under the usual hierarchy rules, so a server can tighten a bot. The bot also holds
@everyonelike any member. - Bits are capped. The installer can never grant a bit they lack.
ADMINISTRATORcannot be installed through the link; a server admin can add it to the role afterwards. - Removing.
RemoveBotdeletes the install, the role and the member row, and writes an audit entry. A reader bot leaves the roster, which rotates the server’s keys. A no-read bot was never sealed to, so there is nothing to rotate. The bot’s token is unaffected: it just has no servers. - What the bot does. On every connect the runtime fetches its installs (
ListBotInstalls), publishes its device list, and claims its grants. You see this asInstallsLoaded,InstalledandRemovedevents.
Encryption, in bot terms
A server is end-to-end encrypted only if its owner chose that when creating it (public is the default), and the mode cannot change afterwards. DMs are always end-to-end encrypted. Here is what that means for a bot.
- A bot runtime is a device, and says so. It makes its own keys like any install and publishes a device list marked as a bot account. Members’ apps treat that mark as permanent for the account: a later list without it is refused. The BOT badge and the bot’s kind come from that verified mark and from the signed grant, not from the server’s say-so.
- The guarantee. No key material reaches a bot runtime without a grant signed by a member. A change forced by the server is at least as visible as the attack a server can already attempt today, which is adding a device to someone’s account; apps pin the keys they are served.
- A no-read bot never gets channel keys. It is admitted so it can post, but members’ apps never seal a channel key to it. It posts with its own sender chain and receives only what members send it directly. The server lists it separately from members, so no owner app counts it as one.
- A reader bot holds the server’s keys. It is admitted and rostered like any member and every member’s app seals to it. Its reach is the whole server’s keys. What limits the channels it hears from is the server’s permission checks on its role, not the keys. Treat a reader bot as a member with a transcript of everything.
- What the server sees. For an encrypted server: who posted in which channel and when, reactions, joins, roles, installs and removals. It does not see message bodies or attachment contents. For an interaction it relays a sealed blob and stores nothing.
- What the bot’s host sees. You do. The runtime decrypts on your machine, so anything it can read is visible to your code, your logs and your host. Decide what to store.
Older apps fail closed. Apps that predate bot support refuse an unknown device-list version instead of guessing, which is why installs wait for the client-version floor (see Install above).
Interactions: commands, mentions and clicks
An interaction is something a member sends to your bot on purpose: a slash command, a mention of a no-read bot, or a click on a button or select on one of its messages. This is the only way a no-read bot hears a member.
- Sealed to the bot. In an encrypted server the member’s app seals the payload to each of the bot’s devices (one-shot, to its long-lived ML-KEM key) and signs it with the member’s device key. It is bound to the server, channel, signer and recipient device. On a server that is not encrypted the payload is plaintext. The server relays it and stores nothing; if no socket of the bot is connected it answers “bot is offline” at once. The member-side “thinking…” state and its 15-second timeout are not shipped yet arriving.
- What you get. A
BotInteraction: kind (Command,ClickorMention), the verified invoker, the channel, and for a command its name and options. The runtime has already verified the addressee, the signer’s membership and device, the signature, freshness and one-time id. - Bots cannot invoke. A bot cannot send an interaction, and the bot side refuses one signed by a bot account.
- Replying. Call
reply_to_interactionwith text for an ephemeral reply, or with nothing for a bare acknowledgement. An ephemeral reply is sealed to the invoker’s devices and shown only to them. A bodiless acknowledgement is accepted only on plaintext servers; in an encrypted server it is sent as an empty sealed payload. The server allows up to 5 replies per interaction; the current runtime sends one per interaction. Or just send a normal message from the bot. - Replay rules. The bot rejects an interaction created more than 2 minutes in the future or more than 10 minutes ago, and remembers each
interaction_iduntil 10 minutes after its creation time. That memory survives restarts. An invoker is limited to 20 interactions a minute.
A limit we state plainly: no per-interaction forward secrecy. Every interaction is sealed to the bot device’s one long-lived ML-KEM key, and v1 never rotates it. Anyone who recorded the sealed blobs, and who ever gets that key, can open every interaction sealed to it: past ones and future ones. The key lives in the bot’s state directory, and the same key also opens channel seeds, DM setup and voice secrets, so a stolen state directory exposes those too. It is not rotated in v1 because rotating it would strand other members. Protect the state directory like the token, or better. Rotation with a grace window is a future change.
What has shipped. The sealed channel and the runtime support (Interaction events, reply_to_interaction) are in the product. Registering slash commands with the server (register_commands, #1436), the mention hook (#1437) and buttons and selects (#1438) are not arriving: the SDK call exists but answers NotYetSupported. When commands ship, a command will have a name of 1 to 32 characters (a-z 0-9 _ -), a description up to 100, up to 25 typed options, and a bot can register 50 global and 50 per-server commands.
Events and reconnecting
- A bot socket is a member socket. It receives the same live broadcasts a member would, filtered by the intents you ask for. The runtime also filters on its side, so your code sees only the intents you configured.
- There is no event log. Events arrive live. Hexis does not keep a per-bot backlog, so a bot that is offline misses what it did not fetch.
- The runtime reconciles on reconnect. It calls
FetchMessagesfrom the last message it saw in each readable channel,ListMembersper server (by state hash) andListChannels. Reactions and voice state are not replayed. - At-least-once, up to the queue. The saved position follows what the runtime put into its event queue, not what your code took out of it. An event dropped from a full queue (the queue holds 10,000; you get an
Error), or still queued when the process dies, is not fetched again. A bot that must not miss a message should keep pulling events, and treat a restart as a possible gap. An acknowledgement API does not exist yet. - Runtime events always arrive:
Connected,Disconnected,WaitingForInstall,FatalandError, whatever intents you set. - Backoff. The runtime reconnects on its own: the wait doubles from 1 second to a 60-second cap with some jitter, and resets after a socket stays up for a minute. A 429 on connect means slow down: it waits what
Retry-Aftersays. A 403 means the token is dead and the runtime stops withFatal. See status and close codes.
Intents and privileged intents
An intent is a family of events. The default is lifecycle and interactions. The full list is in the reference. Today the runtime filters by intent on the client side.
Server-side intents (#1430). A bot declares its intents when it connects (SubscribeEvents), and the server sends only those, from channels the bot’s role can read. Three intents are privileged: members, presence and message_content (plaintext servers; the reader kind in encrypted ones). The owner switches them on per app, with consent text; for public bots they are also a review field. An app that has been approved holds only the privileged intents that were in the set the reviewer saw; adding one later sends the app back to review. A refused intent is listed in the answer, never silently dropped. Message text and attachments, history included, reach a bot only where it may read them; otherwise the event arrives emptied.
Events sent to a bot are capped at 50 a second with bursts of 200. Past that they are dropped and the socket gets a drop notice once a second (BotEventsDroppedBroadcast), after which the bot should reconcile as after a reconnect. Interactions and lifecycle events are never dropped.
Notices
A notice is a server-authored row in a channel: “Posted by Workflows” or “Webhook <name>”. It is the only way the server itself writes into an encrypted channel. The server side (stored notices and their rate limits) is shipped. The client side arriving is not: the distinct card on all three apps, with no avatar, no member colour, no edit and the chip Not end-to-end encrypted, arrives with #1472, and DM notices with #1474.
- Who can cause one. A workflow whose role may send in the channel, and (arriving) an incoming webhook bound to a channel. Never a member and never a bot: there is no member call that posts a notice. A bot posts as itself.
- Content. A template written by the server’s moderators, plus names the server already knows. Up to 2000 characters, no link previews. A placeholder can never add a link, a mention or markup. Only the run’s subject member can be mentioned.
- Names.
WorkflowsandHexisare reserved in display names and webhook names, and a webhook name matching a current member is refused. - DM notices arriving (#1474) go to a per-server “notices” conversation the member can close, which opts them out.
- Limits: see Limits.
Webhooks arriving
Incoming webhooks (#1439) let a service post into a channel without running a bot. They need a new permission bit, MANAGE_WEBHOOKS, off by default, which holders of Manage Server can add to roles. Their posts are notices, so in encrypted servers they are labelled as not end-to-end encrypted. Outgoing, HTTP-delivered bots (#1440) are planned for plaintext servers and metadata events only: with no runtime there is no device, so there is nothing to seal to. This section describes plans; no webhook route is documented because none is public yet.
Safety
- Kill switch. Hexis operators can kill a bot app. The token is cleared and every live socket of the app closes within seconds, on every node. A killed app cannot reset its token or be installed anywhere new. The app’s metadata reports
suspended: true, and a running bot’s socket is closed (the runtime reportsFatal). ASUSPENDEDlifecycle event just before the close is arriving with the event gateway. - What happens to its installs. They stay. The bot is silent, and each server shows it as suspended and can remove it. Removal is not automatic, because taking a reader bot out rotates that server’s keys, and doing that to every server at once is an operator decision. An operator can also kill and remove every install in one step. A kill is reversible: when it is cleared the owner resets the token to bring the bot back.
- Reporting. Any member of a server with the bot installed can report it:
spam,abuse,impersonationorother, with an optional note up to 500 characters. A report carries no message content and no message ids, so an operator reads nothing from an encrypted server. Today it is the/bot reportcommand in the command-line client. Reports are capped (see Limits). - Review. Public bots are reviewed on kind, privileged intents, permissions and privacy text.
- Suspended owners. While an owner’s account is suspended their apps’ tokens are refused. They work again when the suspension ends. Only a reset, a revoke or a kill ends a token for good.