Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ slack:
# agent_id: 00000000-0000-0000-0000-000000000000
# max_streams: 8

# The email channel, hosted mode only (needs the channels.hosted settings
# above): the gateway delivers the mail that members of the agent send to its
# address, and each mail starts or continues a session. Nerve posts no answer;
# the agent answers with the reply tool that the gateway names. A reload
# switches it on or off. See docs/config.md#email.
# email:
# enabled: false

# Where notify, ask_user, and propose_action deliver. The list replaces the
# default rather than adding to it, so name every transport you want. The
# panel (web) is always in it. A transport that is off costs nothing here.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Abstract communication layer with three components:
Implementations:
- **Telegram** — python-telegram-bot v21+ with partial message streaming (edit-in-place, 1.5s rate limit), inline keyboard buttons for notification questions, `/reply` command for free-text answers; a reply to a question card answers it too. With `telegram.reply_routes_to_origin_session`, a reply routes to the session that produced the replied-to message; the message→session map is the `channel_reply_routes` table (recorded whether or not the flag is on, pruned after 90 days by the maintenance tick), so it survives restarts
- **Slack** — slack_sdk Socket Mode (outbound WebSocket, no public URL) with partial streaming via `chat.update`, Block Kit buttons, `/nerve` slash command, and per-thread sessions. `SlackAccessPolicy` (`slack_access.py`) composes user, channel, and DM guardrails; in channels the bot answers only on mention or in a thread it already owns.
- **Hosted** (`hosted/`): provider traffic that a shared channel gateway holds (`slack.mode: hosted`). Nerve has no provider tokens. Each gateway replica opens a WebSocket to `/_internal/channel/v1/stream`, authenticated with a token that the gateway signs (ES256, issuer `nerve-gateway`, audience `nerve-channel`, subject naming this exact tenant and agent, at most 300 seconds of lifetime). One inbox reader pulls admitted events over the preferred stream: read, process, acknowledge, and read again until a page is empty. The runtime builds one `HostedChannel` for each provider in the table of hosted chat providers (`hosted/providers.py`), which gives each provider its hosted mode, its switch, and its channel settings, with defaults for a provider whose config has none; Slack is the only entry. `HostedChannel` registers under the provider's name, so the router and tools see `slack` as with Socket Mode, and uses the same session keys. When the gateway names the provider in a connection's capabilities, a channel uses only the connections of its own provider. The connection's behaviour can tell the router to post nothing for a turn (`InboundMessage.post_answer`), so that the agent answers only with a tool, and can keep a conversation in its last session past the sticky period. The gateway decides which events invoke the agent: a direct message, a mention of the agent, a thread reply, a reaction, an interaction, or a command. A mention of the agent carries the `self` flag, so intake needs no capabilities. Each invoke event names the person that the gateway resolved from the provider user; in external mode, the router resolves it to a human actor (`ChannelRouter.actor_for_principal`), and the turn runs as that person. An observe event can name the person that the gateway linked to its author; the inbox record shows that person for attribution only, and no actor comes from it. Outbound messages are contract operations: `OperationRunner` (`hosted/operations.py`) sends each on a stream that advertised it for the connection, within the stream and connection in-flight limits, and never sends a side-effecting operation again after its result is lost. File uploads and attachment reads use transfers on the stream of their operation; a read is sent only while its length fits Nerve's stream memory limit. Notification cards carry buttons, and a press arrives as an `interaction` event that answers the notification. A card goes with `notify`, and the gateway posts it in the notification channel that the control plane set for the agent; the config does not choose this conversation. `hosted/contract/` holds the version 1 channel contract frames that Nerve reads and sends. The gateway checks the contract; Nerve checks only the members and values that its code uses, and its size limits.
- **Hosted** (`hosted/`): provider traffic that a shared channel gateway holds (`slack.mode: hosted`). Nerve has no provider tokens. Each gateway replica opens a WebSocket to `/_internal/channel/v1/stream`, authenticated with a token that the gateway signs (ES256, issuer `nerve-gateway`, audience `nerve-channel`, subject naming this exact tenant and agent, at most 300 seconds of lifetime). One inbox reader pulls admitted events over the preferred stream: read, process, acknowledge, and read again until a page is empty. The runtime builds one `HostedChannel` for each provider in the table of hosted chat providers (`hosted/providers.py`), which gives each provider its hosted mode, its switch, and its channel settings, with defaults for a provider whose config has none. The entries are Slack and email: usable `channels.hosted` settings give the email channel, `email.enabled` switches it per event, and the subscriptions frame names `email` while it is on, because the gateway takes member mail only for a subscribed agent. `HostedChannel` registers under the provider's name, so the router and tools see `slack` as with Socket Mode, and uses the same session keys. When the gateway names the provider in a connection's capabilities, a channel uses only the connections of its own provider. The connection's behaviour can tell the router to post nothing for a turn (`InboundMessage.post_answer`), so that the agent answers only with a tool, and can keep a conversation in its last session past the sticky period. The gateway decides which events invoke the agent: a direct message, a mention of the agent, a thread reply, a reaction, an interaction, or a command. A mention of the agent carries the `self` flag, so intake needs no capabilities. Each invoke event names the person that the gateway resolved from the provider user; in external mode, the router resolves it to a human actor (`ChannelRouter.actor_for_principal`), and the turn runs as that person. An observe event can name the person that the gateway linked to its author; the inbox record shows that person for attribution only, and no actor comes from it. Outbound messages are contract operations: `OperationRunner` (`hosted/operations.py`) sends each on a stream that advertised it for the connection, within the stream and connection in-flight limits, and never sends a side-effecting operation again after its result is lost. File uploads and attachment reads use transfers on the stream of their operation; a read is sent only while its length fits Nerve's stream memory limit. Notification cards carry buttons, and a press arrives as an `interaction` event that answers the notification. A card goes with `notify`, and the gateway posts it in the notification channel that the control plane set for the agent; the config does not choose this conversation. `hosted/contract/` holds the version 1 channel contract frames that Nerve reads and sends. The gateway checks the contract; Nerve checks only the members and values that its code uses, and its size limits.
- **Web** — Passive channel using gateway WebSocket

Adding a new channel (Discord, WhatsApp, etc.) requires implementing ~5 methods and zero session/routing logic.
Expand Down
Loading
Loading