Aleph
Architecture

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 uses SessionManager; the session_events table is the single source of truth, written by the src/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 old InterfaceRegistry name 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

MethodDescriptionKey Parameters
agent.runStart agent executionmessage, session_key, thinking?, model?
agent.statusGet run statusrun_id
agent.cancelCancel a running agentrun_id
agent.abortForce-abort immediatelyrun_id

Session Methods

MethodDescriptionKey Parameters
session.getGet session infosession_key
session.listList all sessionsfilter?
session.historyGet message historysession_key, limit?
session.compactTrigger compressionsession_key
session.deleteDelete a sessionsession_key

Configuration Methods

MethodDescriptionKey Parameters
config.getGet current config--
config.patchPartial update (JSON Merge Patch)patch
config.applyFull config replaceconfig
config.reloadReload from disk--

Event Methods

MethodDescriptionKey Parameters
events.subscribeSubscribe to event topicpattern (glob)
events.unsubscribeUnsubscribepattern
events.listList active subscriptions--

Memory Methods

MethodDescriptionKey Parameters
memory.storeStore a factcontent, metadata?
memory.searchSearch factsquery, limit?
memory.deleteDelete a factfact_id
memory.statsGet memory statistics--

Browser Methods (CDP)

MethodDescriptionKey Parameters
browser.navigateGo to URLurl
browser.clickClick elementselector
browser.typeType textselector, text
browser.screenshotTake screenshotselector?
browser.evaluateRun JavaScriptscript

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:

DomainDescription
connectTop-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:

  1. TraceLayer -- Attaches request spans and trace IDs for observability
  2. MetricsLayer -- Records request timing and method-level metrics
  3. AuthLayer -- Validates authentication (skipped when auth.mode = "none")
  4. RateLimitLayer -- Enforces per-identity, per-scope rate limits
  5. ValidateLayer -- Validates request schema before reaching the handler
  6. 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

InterfaceLocationPlatform
CLIinterfaces/cli/All platforms
Telegramsrc/gateway/interfaces/telegram/All platforms
Discordsrc/gateway/interfaces/discord/All platforms
iMessagesrc/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 Teamssrc/gateway/interfaces/<name>/Per platform
WebChatEmbedded 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 FormatUse Case
agent:main:mainCross-channel shared session (DmScope::Main or main session)
agent:main:dm:user123Per-user isolated DM (DmScope::PerPeer, default)
agent:main:telegram:dm:user123Per-channel per-user DM (DmScope::PerChannelPeer)
agent:main:discord:group:guild-idDiscord server channel (Group)
agent:main:cron:daily-summaryCron job / webhook (Task)
subagent:agent:main:translatorSub-agent delegation (Subagent)
agent:main:ephemeral:uuidTemporary, 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

PatternEvents
stream.*All streaming events
stream.chunkText content chunks
stream.agent_traceStructured loop-originated execution trace
stream.tool_startTool execution started
stream.tool_endTool execution completed
agent.*Agent lifecycle events
agent.startedRun started
agent.completedRun completed
agent.errorRun error
session.*Session events
config.*Configuration change events
system.tickPeriodic heartbeat (uptime, connections, state version)
system.shutdownServer 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_ticket on first pairing (receiving a device_token back), then device_token on every reconnect. token (the legacy shared Gateway token) is accepted as a fallback. device_id is client-asserted and shares a namespace with cluster nodes, so the exchange refuses a device_id already 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.

RouteMethodDescription
/v1/modelsGETList available models
/v1/models/{model_id}GETGet model details
/v1/chat/completionsPOSTChat completions (streaming + non-streaming)
/v1/embeddingsPOSTText embeddings
/v1/responsesPOSTOpenAI Responses API
/v1/healthGETAPI 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:

EndpointPurpose
/wsWebSocket upgrade endpoint
/healthHealth check (returns 200 OK)
/metricsPrometheus-compatible metrics
/Static files for built-in WebChat UI (ControlPlane)
/loginSession-cookie login page
/auth/loginLogin form submission
/auth/logoutLogout (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.



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 / key empty → 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.credentials
  • gateway.identity.get
  • gateway.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

On this page