Gateway Architecture
WebSocket control plane on port 18790, JSON-RPC 2.0 protocol, multi-channel routing, middleware pipeline, event distribution, and connection lifecycle.
The Gateway is the single entry point for all communication with Aleph. It runs a WebSocket server on ws://127.0.0.1:18790/ws, speaks JSON-RPC 2.0, and routes requests from any connected client -- whether a native macOS app, a Telegram bot, or the terminal CLI -- to the appropriate handler. This page covers the protocol layer, message pipeline, multi-channel routing, the event system that powers real-time streaming, and hardening mechanisms such as self-signed TLS with client-side TOFU and single-source error receipts.
For how sessions are managed once a request arrives, see Session Service. For what happens after routing reaches the agent, see Agent Harness.
Gateway Components
┌─────────────────────────────────────────────────────────────────────┐
│ Gateway Server │
│ ws://127.0.0.1:18790/ws │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Inbound │ │ Handler │ │ Outbound │ │
│ │ Router │ ──▶ │ Registry │ ──▶ │ Emitter │ │
│ │ │ │ │ │ │ │
│ │ • Parse req │ │ • Route │ │ • Stream │ │
│ │ • Validate │ │ • Execute │ │ • Events │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Session │ │ Event │ │ Channel │
│ │ Manager │ │ Bus │ │ Registry │
│ │ │ │ │ │ │
│ │ • SQLite │ │ • Pub/Sub │ │ • Telegram │
│ │ • Compaction │ │ • Topics │ │ • Discord │
│ │ • History │ │ • Subscribe │ │ • iMessage │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘The Gateway has six core components:
- Inbound Router -- Parses incoming JSON-RPC frames, validates structure, and dispatches to the correct handler
- Handler Registry -- Maps method names (e.g.,
agent.run,session.list) to handler functions - Outbound Emitter -- Sends streaming events and final responses back to the client
- Session Manager -- Persists conversation history in SQLite (
session.*RPC still usesSessionManager; thesession_eventstable is the single source of truth, written by thesrc/session/actor) - Event Bus -- Pub/sub system for real-time event distribution with glob-pattern subscriptions
- Channel Registry (
ChannelRegistry) -- Tracks active chat integrations (Telegram, Discord, iMessage, ...). The oldInterfaceRegistryname is no longer used.
JSON-RPC 2.0 Protocol
All communication uses JSON-RPC 2.0 over WebSocket frames. There are three message types.
Request (Client to Gateway)
{
"jsonrpc": "2.0",
"id": "uuid-xxx",
"method": "agent.run",
"params": {
"message": "Hello, what can you do?",
"session_key": "agent:main:main"
}
}The id field is a client-generated UUID. The Gateway returns the same id in the response so the client can match request to response.
Response (Gateway to Client)
{
"jsonrpc": "2.0",
"id": "uuid-xxx",
"result": {
"run_id": "run-123",
"status": "running"
}
}For agent.run, the initial response confirms the run has started. The actual AI output arrives via streaming events.
Event (Gateway to Client, no id)
{
"jsonrpc": "2.0",
"method": "event",
"params": {
"topic": "stream.chunk",
"data": {
"run_id": "run-123",
"content": "Hello! I can help you with..."
}
}
}Events are server-initiated notifications (no id field). Clients subscribe to event topics via the events.subscribe method.
RPC Method Reference
Agent Methods
| Method | Description | Key Parameters |
|---|---|---|
agent.run | Start agent execution | message, session_key, thinking?, model? |
agent.status | Get run status | run_id |
agent.cancel | Cancel a running agent | run_id |
agent.abort | Force-abort immediately | run_id |
Session Methods
| Method | Description | Key Parameters |
|---|---|---|
session.get | Get session info | session_key |
session.list | List all sessions | filter? |
session.history | Get message history | session_key, limit? |
session.compact | Trigger compression | session_key |
session.delete | Delete a session | session_key |
Configuration Methods
| Method | Description | Key Parameters |
|---|---|---|
config.get | Get current config | -- |
config.patch | Partial update (JSON Merge Patch) | patch |
config.apply | Full config replace | config |
config.reload | Reload from disk | -- |
Event Methods
| Method | Description | Key Parameters |
|---|---|---|
events.subscribe | Subscribe to event topic | pattern (glob) |
events.unsubscribe | Unsubscribe | pattern |
events.list | List active subscriptions | -- |
Memory Methods
| Method | Description | Key Parameters |
|---|---|---|
memory.store | Store a fact | content, metadata? |
memory.search | Search facts | query, limit? |
memory.delete | Delete a fact | fact_id |
memory.stats | Get memory statistics | -- |
Browser Methods (CDP)
| Method | Description | Key Parameters |
|---|---|---|
browser.navigate | Go to URL | url |
browser.click | Click element | selector |
browser.type | Type text | selector, text |
browser.screenshot | Take screenshot | selector? |
browser.evaluate | Run JavaScript | script |
Other Method Domains
The full and authoritative list lives in Gateway Methods Reference and the registration block at
src/gateway/handlers/mod.rs::HandlerRegistry::new. Below is a domain-grouped summary:
| Domain | Description |
|---|---|
connect | Top-level method (no prefix); the first frame on a /ws connection must be connect; carries device_token / bootstrap_ticket / token |
pairing.* | Channel sender approval (iMessage / Telegram unknown senders) -- not device auth |
plugin.* / plugins.* | Plugin install / enable / disable / call; CC-compatible dual prefix |
mcp.* | MCP server lifecycle + call + on-demand list_* resource / prompt discovery |
skill.* | Skill list / install / activate (v2 SkillSystem) |
generation.* | Image / video / voice generation |
cron.* | Scheduled tasks: list / add / remove / run |
heartbeat.* / dreaming.* | Background-task RPCs |
artifacts.* | Artifact store: list / get / put |
voice.* | Voice sessions + delta topics |
hub.* | Aleph Hub: primer / catalog_client / install / verify / trust |
strategy.* / goal.* / loop.* | Three independent long-task subsystems |
graph.* | Governance topology node / edge queries |
cluster.* | Cluster federation RPCs (environments.list / node_invoke, etc.) |
identity.* / agent_identity.* | Per-agent signed operation ledger |
exec_approvals.* / secret_approvals.* | Action / secret approval |
gateway.* | Gateway self: credentials / identity.get / metrics.lanes / metrics.run_concurrency, etc. |
wizard.* | Wizard sessions (RPC-driven, not a CLI subcommand) |
Message Pipeline
When a WebSocket frame arrives, the Gateway processes it through a layered middleware pipeline:
WebSocket Frame
│
▼
┌────────────────────────────────┐
│ 1. Frame Parsing │
│ • Deserialize JSON-RPC │
│ • Validate structure │
│ • Extract method + params │
└────────────────┬───────────────┘
│
▼
┌────────────────────────────────┐
│ 2. Middleware Pipeline │
│ • TraceLayer (span/req-id) │
│ • MetricsLayer (timing) │
│ • AuthLayer (auth check) │
│ • RateLimitLayer (quota) │
│ • ValidateLayer (schema) │
│ • HandlerService (dispatch) │
└────────────────┬───────────────┘
│
▼
┌────────────────────────────────┐
│ 3. Handler Dispatch │
│ • Look up method in registry│
│ • agent.run → ExecutionEngine│
│ • session.* → SessionManager│
│ • config.* → ConfigManager │
└────────────────┬───────────────┘
│
▼
┌────────────────────────────────┐
│ 4. Execution │
│ • Handler processes request │
│ • May spawn AgentLoop │
│ • Streams events via EventBus│
└────────────────┬───────────────┘
│
▼
┌────────────────────────────────┐
│ 5. Response │
│ • JSON-RPC result frame │
│ • Or JSON-RPC error frame │
└────────────────────────────────┘The middleware pipeline runs for every JSON-RPC request in this order:
- TraceLayer -- Attaches request spans and trace IDs for observability
- MetricsLayer -- Records request timing and method-level metrics
- AuthLayer -- Validates authentication (skipped when
auth.mode = "none") - RateLimitLayer -- Enforces per-identity, per-scope rate limits
- ValidateLayer -- Validates request schema before reaching the handler
- HandlerService -- Dispatches to the registered handler for execution
Multi-Channel Routing
Aleph supports multiple chat interfaces simultaneously. Each interface (Telegram, Discord, iMessage, etc.) translates its platform-specific messages into the unified JSON-RPC protocol and forwards them to the Gateway.
Channel Adapter (src/gateway/channel.rs)
Every channel adapter implements the Channel trait; there are no compile-time feature flags -- channels are registered at runtime through [channels.<type>] into ChannelRegistry:
#[async_trait]
pub trait Channel: Send + Sync {
fn info(&self) -> &ChannelInfo;
fn state(&self) -> &ChannelState;
fn id(&self) -> &ChannelId;
fn capabilities(&self) -> &ChannelCapabilities;
async fn health(&self) -> ChannelHealth;
async fn start(&mut self) -> ChannelResult<()>;
async fn stop(&mut self) -> ChannelResult<()>;
async fn send(&self, message: OutboundMessage) -> ChannelResult<SendResult>;
async fn send_typing(&self, conversation_id: &ConversationId) -> ChannelResult<()>;
fn approval_capability(&self) -> Option<Arc<dyn ChannelApprovalCapability>>;
}Built-in adapters are wired through ChannelFactory entries registered in interfaces::register_channel_plugins. An unregistered factory is unreachable from config. imessage and cli deliberately bypass that table -- iMessage is constructed directly in initialize_channels, and CLI is not a configurable channel type.
Available Interfaces
| Interface | Location | Platform |
|---|---|---|
| CLI | interfaces/cli/ | All platforms |
| Telegram | src/gateway/interfaces/telegram/ | All platforms |
| Discord | src/gateway/interfaces/discord/ | All platforms |
| iMessage | src/gateway/interfaces/imessage/ (Local + BlueBubbles transports) | Local: macOS only; BlueBubbles: any OS |
| Slack / Matrix / Mattermost / Signal / IRC / Nostr / XMPP / Email / Webhook / WeChat / Feishu / Line / QQ / MS Teams | src/gateway/interfaces/<name>/ | Per platform |
| WebChat | Embedded in aleph-server (via rust_embed) | All platforms |
Session Key Resolution
Each channel produces a unique session key that determines whether conversations are shared or isolated (format decided by DmScope and PeerKind, see [src/routing/session_key.rs]):
| Session Key Format | Use Case |
|---|---|
agent:main:main | Cross-channel shared session (DmScope::Main or main session) |
agent:main:dm:user123 | Per-user isolated DM (DmScope::PerPeer, default) |
agent:main:telegram:dm:user123 | Per-channel per-user DM (DmScope::PerChannelPeer) |
agent:main:discord:group:guild-id | Discord server channel (Group) |
agent:main:cron:daily-summary | Cron job / webhook (Task) |
subagent:agent:main:translator | Sub-agent delegation (Subagent) |
agent:main:ephemeral:uuid | Temporary, no persistence (Ephemeral) |
DM scope can be configured per interface:
pub enum DmScope {
Main, // All DMs share the main session
PerPeer, // Isolated session per user (default)
PerChannelPeer, // Isolated per channel + user
}Event System
The EventBus provides a pub/sub mechanism for real-time event distribution. Clients subscribe using glob patterns.
Event Topics
| Pattern | Events |
|---|---|
stream.* | All streaming events |
stream.chunk | Text content chunks |
stream.agent_trace | Structured loop-originated execution trace |
stream.tool_start | Tool execution started |
stream.tool_end | Tool execution completed |
agent.* | Agent lifecycle events |
agent.started | Run started |
agent.completed | Run completed |
agent.error | Run error |
session.* | Session events |
config.* | Configuration change events |
system.tick | Periodic heartbeat (uptime, connections, state version) |
system.shutdown | Server shutdown notification |
Subscribing to Events
{
"jsonrpc": "2.0",
"id": "sub-1",
"method": "events.subscribe",
"params": { "pattern": "stream.*" }
}After subscribing, the client receives all matching events as server-initiated notifications until it unsubscribes.
Connection Lifecycle
Authentication Flow
Client WebSocket Connect
│
▼
┌─────────────────────────────────┐
│ Loopback connection? │
│ Yes → Zero-config operator │
│ (no credentials) │
│ No → First frame must be │
│ "connect" method │
└─────────────────────────────────┘
│
▼ (remote)
┌─────────────────────────────────┐
│ resolve_connect_auth │
│ 1. Device token (aleph-dt-*) │
│ -- paired device, long-lived│
│ 2. Bootstrap ticket │
│ (aleph-bt-*) -- 5 min, │
│ single-use │
│ 3. Shared Gateway token │
│ (aleph-<uuid>, HMAC) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Authorized → role = operator │
│ WS dispatch accepts all │
│ Remote WS closed with 4001 │
│ (token rotation / device │
│ revocation) │
└─────────────────────────────────┘The default bind is 127.0.0.1 (loopback only); loopback is the zero-config operator and needs no credential. Set [gateway] host = "0.0.0.0" to open the LAN -- a remote device can then reach the socket but is walled until it presents a valid credential at connect. The is_loopback check reads the raw socket peer (not X-Forwarded-For), preventing forged headers from granting zero-config operator. A valid credential = full operator authority (identical to local); remote WS sockets are closed immediately on token rotation (gateway.token.rotate) or per-device revoke (gateway.devices.revoke), not at the next handshake.
Connect Request
A remote client must send a connect message as the first frame:
{
"method": "connect",
"params": {
"minProtocol": 1,
"maxProtocol": 1,
"client": {
"id": "macos-app",
"version": "1.0.0",
"platform": "macos"
},
"device_token": "aleph-dt-...",
"bootstrap_ticket": "aleph-bt-..."
}
}Loopback clients omit the credential fields entirely. A remote client sends
bootstrap_ticketon first pairing (receiving adevice_tokenback), thendevice_tokenon every reconnect.token(the legacy shared Gateway token) is accepted as a fallback.device_idis client-asserted and shares a namespace with cluster nodes, so the exchange refuses adevice_idalready naming a non-Panel device.
OpenAI-Compatible Routes
In addition to the WebSocket JSON-RPC API, the Gateway exposes an OpenAI-compatible HTTP API on the same port (18790) via src/gateway/openai_api/. This allows existing clients and tools that speak the OpenAI API format to connect to Aleph without modification.
| Route | Method | Description |
|---|---|---|
/v1/models | GET | List available models |
/v1/models/{model_id} | GET | Get model details |
/v1/chat/completions | POST | Chat completions (streaming + non-streaming) |
/v1/embeddings | POST | Text embeddings |
/v1/responses | POST | OpenAI Responses API |
/v1/health | GET | API health check |
These routes share the WebSocket bearer-token mechanism. With [gateway] allow_insecure_remote = false (the default), remote HTTP access must arrive over TLS; plaintext to a non-loopback peer is refused.
Hot Reload
The Gateway watches ~/.aleph/config/aleph.toml for changes and applies them without restarting:
~/.aleph/config/aleph.toml modified
│
▼
┌─────────────────────────────────┐
│ Debounce (500ms) │
└─────────────────┬───────────────┘
│
▼
┌─────────────────────────────────┐
│ Parse + validate new config │
└─────────────────┬───────────────┘
│
▼
┌─────────────────────────────────┐
│ Apply changes: │
│ • Restart affected channels │
│ • Update routing rules │
│ • Emit config.changed event │
└─────────────────────────────────┘The [execution] run caps (max_runs_global / max_runs_per_agent) hot-reload via an arc-swap global semaphore without restart.
HTTP Endpoints
Alongside the WebSocket server, the Gateway also serves HTTP endpoints on the same port:
| Endpoint | Purpose |
|---|---|
/ws | WebSocket upgrade endpoint |
/health | Health check (returns 200 OK) |
/metrics | Prometheus-compatible metrics |
/ | Static files for built-in WebChat UI (ControlPlane) |
/login | Session-cookie login page |
/auth/login | Login form submission |
/auth/logout | Logout (clears session cookie) |
When A2A (Agent-to-Agent) is enabled, additional A2A routes are mounted on the same HTTP server.
Interface Configuration Example
{
"interfaces": {
"telegram": {
"token": "BOT_TOKEN",
"allowFrom": ["+1234567890"],
"groups": {
"*": { "requireMention": true }
}
},
"discord": {
"token": "BOT_TOKEN",
"guilds": ["guild-id-1"]
}
}
}Each interface has its own authentication and scope configuration. Telegram can restrict which phone numbers are allowed; Discord can restrict which guilds the bot joins.
Related Pages
- Architecture Overview -- Full system diagram and module map
- Agent Harness -- What happens after the Gateway dispatches to the agent
- Session Service -- Session key resolution and persistence
- Tool Architecture -- Tool execution triggered by agent requests
26.7.x Addendum
Gateway Core Status
The in-process gateway in aleph-server handles:
- WebSocket protocol (JSON-RPC 2.0 over ws/wss)
- TLS (self-signed or user-supplied, terminated in-process by
axum-server) - Authentication (loopback zero-config operator / device token / bootstrap ticket / shared token / WS Origin check)
- Routing (chat / config / memory / agents / tools / mcp / skills / plugins / extensions / channels / teams / projects / workspaces / runtimes / voice / artifacts / hub / strategy / goal / loop / ...)
- Full RPC surface (see Methods Reference)
Single-Source Error Receipt
26.7.21+: every execution error is normalized through ExecutionError::user_receipt(locale) in src/gateway/execution_engine/ to a stable error code plus a localized short message. This is the single source the Panel and channels render -- the raw typed error chain never leaks. The bin-crate RPC handlers (agent.run / chat.send in src/bin/aleph-server/server_init.rs) and the inbound router's channel reply path both call the same function, so a single failure can never produce two error bubbles.
WS Task-Leak Fixes
26.7.21+ (see src/gateway/server/handler.rs::forward_bus_to_client): per-connection event-forward tasks exit on a failed try_send instead of holding a global broadcast::Receiver for the rest of the process; RecvError::Lagged(n) is non-fatal (counted into an overflow metric, with the idle/ping watchdog eventually closing the socket with code 1008); webhook backpressure returns 503 instead of silently dropping inbound messages.
Self-Signed TLS + Client-Side TOFU
26.7.17 introduces in-process TLS (src/gateway/tls.rs); 26.7.18 adds SAN auto-discovery + drift sidecar; the Panel's WKWebView adapter (macOS reference platform, with iOS Keychain mirror) implements TOFU certificate trust (fingerprint + SAN approval + pinned trust store). Config:
[gateway.tls]— self-signed or user-supplied (cert/keyempty → auto-generated)[gateway] trusted_proxy— real reverse-proxy IP[gateway] allow_insecure_remote = false(default) — fail closed
Device / Credentials / Ticket / Token RPCs
26.7.21 registers in HandlerRegistry:
gateway.credentialsgateway.identity.getgateway.metrics.lanes/gateway.metrics.run_concurrency
gateway.token.current / gateway.token.rotate / gateway.devices.list / gateway.devices.revoke are wired in via boot-phase injection (see the gateway_state registration block in src/gateway/handlers/mod.rs).
See Also
- Gateway Protocol — protocol
- Gateway Auth — auth / pairing / TOFU
- Gateway Methods Reference — full RPC
1-2-3-4 Architecture Model
The engineering skeleton: 1 Core, 2 Faces, 3 Limbs, 4 Nerves
Routing System
Aleph's real routing layer: session-key resolution (SessionKey), DmScope isolation, hierarchical RouteBinding matching, two-stage command/keyword routing rules, runtime overlay, provider failover, and VESR.