Envoys
Envoys
An envoy is the workspace agent reached over someone else’s wire — Telegram or WhatsApp. What arrives is a message from a person the workspace may not have a user row for, so the interesting question is not “how do I speak this protocol” but whose permissions the agent acts under when it answers . That is policies , and it is why an envoy is a declaration in source rather than host configuration: the answer belongs in a diff.
The transport itself stays host-supplied. Holding a socket open is not something a scale-to-zero tenant can do, so the workspace names the transport and the host provides it — Colony owns the credentials, the socket or the verified webhook endpoint, and the sealed session.
The declaration
One file per envoy at src/envoys/+<lower_snake_case>.ts ; the filename is the envoy key.
// src/envoys/+sales_desk.ts
export default {
transport: 'telegram',
audience: 'public',
policies: ['commercial_shared', 'sales_rep'],
groupMessages: 'disabled',
delegation: 'enabled',
task: 'Answer questions about quotes and accounts for this customer.'
} satisfies Envoy;
| Field | Meaning |
|---|---|
transport | The wire that reaches it ( telegram or whatsapp ). The transport dictates how addresses are canonicalised and compared. |
audience | Who may reach it: `public` (anyone who can message the wire) or `authenticated` (a member who has proven the address is theirs). Reach, not conversation shape — that is what `groupMessages` answers. |
policies | Everything this envoy MAY DO — bound to the generated policy names, so an envoy cannot point at a policy that does not exist. An array, safe because the compiler refuses an unconditional grant beside a narrowed one on the same collection. |
task | The envoy’s standing instruction, on top of the workspace’s `src/+agents.md`. |
groupMessages | How a group message triggers a turn: `disabled`, `mention_or_reply`, or `all`. Messages that do not trigger a turn are still recorded in the chat replica and can be read with `read_messages`. |
delegation | Required. `enabled` lets this envoy spawn child tasks; `disabled` refuses delegation entirely, for narrow ingress envoys that must act alone. |
Pairing and sender registration are different
Pairing links one envoy to the account it answers as — a WhatsApp number through a QR scan, or a Telegram bot through its BotFather token. It does not register the people who message it. For an `authenticated` audience, possession of each sender address is proven only when that person first contacts the envoy.
- An unknown sender sends a direct message, or addresses the envoy in a group.
- The envoy privately replies to that sender with a single-use registration link that expires after 15 minutes. A group never receives the link.
- The link opens the tenant’s built-in registration page and requires the person to sign in to the platform.
- An explicit confirmation binds the canonical transport address to that tenant identity, then shows success. The person can return to the conversation and resend their request.
Messages that arrive while a turn is running
An addressed message is queued as a steer the moment it arrives: if the assistant is mid-turn it is consumed before the next model step, and if nothing is running it starts the next turn on the same conversation. Ambient group messages are kept in the chat replica and never interrupt a turn; each provider call carries a trailing note with the unread count, and the assistant reads them on demand with `read_messages`. The assistant answers by writing short text updates before its tool calls, which are delivered as they are written so the sender can follow the work; the final answer arrives as one message. The agent can search the complete stored transcript, including queued follow-ups, when older context has left its prompt window.
The chat replica
Every inbound message and every reply is kept in one ordered replica per chat, both directions, with media materialised when it arrives. Addressed messages also queue as work; everything else stays in the replica for `read_messages` to serve on demand, and each provider call carries a trailing note with the unread count. The replica copies what the channel showed: a provider-reported edit converges there, an already-answered transcript never changes, and the channel’s history floor — when this host first saw the chat — is reported with every read, because earlier messages cannot be retrieved from the provider.
Whose permissions the agent acts under
An envoy may be a group chat, so there is no single person behind it to inherit permissions from. Each declared envoy gets its own agent subject — holding exactly the policies policy names it declares, nothing else — and every inbound message
re-enters the workspace as that subject before the Norbius loop starts. A public envoy’s threads route to the admin inbox and stay off every member’s; an envoy whose policies grant nothing can do nothing, and the same write contracts and approval gates apply as for any other client.
Workbench, documents, and delegation
Every envoy has one shared workbench for delegated agent sessions. Inbound text and documents never enter it: each conversation owns its own transcript, its bolt_envoy_messages replica and drain claim, and the attachment bindings recorded there. A document read resolves both its key and conversation binding before bytes are returned. The shared workbench may offer subagent because the workbench contains no sender uploads.
Setting delegation to 'disabled' stops the envoy from spawning subagents at all — the right shape for a narrow ingress surface.
envoy_name key on each bolt_envoy_messages row, which carries the conversation id binding the external chat to its conversation. Ingress admits and buffers messages without invoking a model; one leased drain orders the burst, persists its complete sender attribution, and runs one turn, streaming the assistant’s updates as they are written and sending the final answer when the turn settles.Related guides
- Norbius — the loop every envoy message runs through
- Policies — what the envoy’s subject can see and change
- Workspace source — where the envoy role lives in the layout