Openclaw · EN · Part 1
Ch 02 System Architecture Overview
02-ch-02-System-Architecture-Overview.md
第2章 System Architecture Overview
The Gateway Control Plane
At OpenClaw's heart sits the Gateway server—not a message broker, not a service mesh, but a typed control plane that exposes ~40 RPC methods over WebSocket. Every component in the system connects to this Gateway as a "node," authenticated through a device challenge-response handshake, then communicates through three frame types: RequestFrame, ResponseFrame, and EventFrame.
Rendering diagram...
The Gateway protocol, defined in src/gateway/protocol/schema/types.ts, is not ad-hoc JSON over WebSocket. Every frame is validated against TypeBox schemas before processing, ensuring type safety from the wire up. The protocol version is negotiated during connection handshake, enabling graceful evolution without breaking existing clients.
When a client connects, it sends a ConnectParams frame with authentication credentials:
// src/gateway/client.ts:79-100
export type GatewayClientOptions = {
url?: string; // ws://127.0.0.1:18789
connectChallengeTimeoutMs?: number; // [1]
token?: string; // Bootstrap token
deviceToken?: string; // Device-specific token
password?: string; // Optional password auth
instanceId?: string; // Instance identifier
clientName?: GatewayClientName; // cli, mobile, web, etc.
clientDisplayName?: string; // Human-readable name
clientVersion?: string; // Protocol version
platform?: string; // os, platform info
deviceFamily?: string; // Device type
mode?: GatewayClientMode; // Operation mode
role?: string; // Access role
scopes?: string[]; // Permission scopes
caps?: string[]; // Capabilities
commands?: string[]; // Available commands
// ...
};
The Gateway responds with HelloOk, containing the negotiated protocol version and server capabilities. From there, all communication follows the request/response pattern with optional server-initiated events.
🤿 深入底层:The Gateway uses TypeBox schemas rather than plain TypeScript types for runtime validation. This means every incoming frame is checked against its schema before processing—catching malformed payloads before they reach business logic. The protocol schemas live in
protocol-schemas.ts, generating both TypeScript types and runtime validators from a single source of truth.
The Gateway protocol handles six major domains:
| Domain | Example Methods | Purpose |
|---|---|---|
| Sessions | sessionsCreate, sessionsSend, sessionsList | Manage conversation state and message history |
| Config | configGet, configSet, configSchema | Read and write configuration |
| Channels | channelsStatus, channelsLogout | Query channel connection state |
| Agents | agentsCreate, agentsList, agentIdentity | Manage agent instances and identities |
| Nodes | nodePairRequest, nodeList, nodeInvoke | Handle node pairing and RPC |
| Wizard | wizardStart, wizardNext, wizardCancel | Guide multi-step configuration flows |
This RPC-style design means the Gateway is not a message broker—it doesn't route arbitrary messages between components. Instead, it exposes a structured API surface, making versioning, documentation, and client code generation straightforward.
Component Boundaries and Responsibilities
With the Gateway as the central hub, OpenClaw's architecture breaks into clear component boundaries. Each component has a specific responsibility and crosses into others only through defined contracts.
Gateway Server
The Gateway server (src/gateway/server.ts) is the WebSocket control plane implementation. Its responsibilities:
- Connection management: Accept WebSocket connections, handle authentication, manage node lifecycle
- Protocol enforcement: Validate all frames against schemas, return structured errors
- Request routing: Route RPC calls to appropriate handlers (sessions, config, agents, etc.)
- Event broadcasting: Push server-initiated events (agent responses, channel status changes, etc.) to relevant clients
- State coordination: Maintain session state, channel status, and configuration in memory
The Gateway does not execute agent logic directly. That responsibility belongs to the Agent Runtime. The Gateway also doesn't implement platform-specific messaging logic—that's delegated to channel plugins.
Agent Runtime
The Agent Runtime (src/agents/) manages the Pi (Personal Intelligence) execution environment. Its responsibilities:
- Agent lifecycle: Start, stop, and monitor agent instances
- Model streaming: Stream responses from language models in real-time
- Tool execution: Invoke tools through the Gateway when requested by agents
- Prompt caching: Cache prompt prefixes to reduce API costs and latency
- RPC integration: Expose agent capabilities through Gateway protocol endpoints
The Agent Runtime receives user messages through the Gateway, streams responses back through Gateway events, and invokes tools via Gateway RPC calls. This design means the agent itself doesn't need to understand channels, authentication, or session management—it focuses on AI execution.
Channels
Channels (src/channels/) implement platform-specific messaging logic. Each channel (Telegram, Discord, Slack, etc.) is responsible for:
- Platform integration: Connect to the platform's API or webhook system
- Message inbound: Receive messages from the platform, wrap them in
InboundEnvelope, submit to Gateway - Message outbound: Receive agent replies from Gateway, format and send to platform
- Pairing & policy: Handle DM pairing allowlists, group access control, context visibility filters
Channels don't execute agent logic or manage configuration—they're pure adapters between platform protocols and OpenClaw's internal envelope format. The InboundEnvelope contract, defined in src/channels/plugins/types.core.ts, carries metadata (channel ID, account ID, peer info, message type) alongside the message content.
Plugins
The Plugin system (src/plugins/) manages discovery, loading, and lifecycle of extensions. Its responsibilities:
- Manifest validation: Read and validate
openclaw.plugin.jsonfiles - Plugin activation: Load plugins on demand, track enabled state
- Contract enforcement: Ensure plugins respect SDK boundaries
- Provider/model routing: Route model requests to appropriate provider plugins
The Plugin system doesn't implement provider logic itself—that's delegated to individual provider plugins (OpenAI, Anthropic, Google, etc.). It also doesn't implement channel logic—that's handled by channel plugins.
Config
The Config system (src/config/) manages configuration schema, validation, and persistence. Its responsibilities:
- Schema generation: Generate Zod schemas from plugin manifests and core config
- Validation: Validate config reads and writes against schemas
- Migration: Handle legacy config key migrations
- Persistence: Read and write config to JSON files
The Config system doesn't enforce business logic—that's spread across components that use config values. It focuses on schema correctness and backward compatibility.
CLI
The CLI (src/cli/) provides the command-line interface for human interaction. Its responsibilities:
- Command routing: Parse argv, route to appropriate command handlers
- Help & documentation: Generate help text from command metadata
- Gateway client: Connect to Gateway as a node, invoke RPC methods
- User interaction: Handle prompts, progress indicators, and terminal output
The CLI doesn't implement core business logic—it's a thin client that translates human commands into Gateway protocol calls. This design means alternative clients (mobile apps, Web UI) can use the same Gateway protocol without duplicating logic.
⚠️ 避坑指南:A common misunderstanding is thinking the CLI is the "main" OpenClaw process. In reality, the Gateway server is the core process—the CLI, mobile apps, and Web UI are all clients connecting to it. Running
openclaw gateway runstarts the server;openclaw agent ...connects as a client.
The Plugin Architecture
OpenClaw's extensibility comes from its plugin system, but what makes this architecture work is not just "supporting plugins"—it's enforcing boundaries so plugins can extend the system without breaking it.
The Plugin SDK, located at src/plugin-sdk/, defines the public contract that all plugins must use. Extensions are forbidden from importing core internals directly (src/**)—they must cross the boundary through SDK exports. This rule, enforced in CLAUDE.md, prevents tight coupling and allows core evolution without breaking third-party plugins.
Plugins are discovered through their manifest file (openclaw.plugin.json), defined in src/plugins/manifest.ts:
// src/plugins/manifest.ts:35-71
export type PluginManifest = {
id: string; // Unique plugin identifier
configSchema: Record<string, unknown>; // Plugin's config schema
enabledByDefault?: boolean; // Auto-enable on discovery
legacyPluginIds?: string[]; // Compat for old ids
autoEnableWhenConfiguredProviders?: string[]; // Wiring for provider refs
kind?: PluginKind | PluginKind[]; // channel | provider | skill
channels?: string[]; // Channel IDs provided
providers?: string[]; // Provider IDs provided
modelSupport?: PluginManifestModelSupport; // Model family ownership
cliBackends?: string[]; // CLI inference backends
providerAuthEnvVars?: Record<string, string[]>; // Auth env var lookup
providerAuthChoices?: PluginManifestProviderAuthChoice[]; // Auth metadata
skills?: string[]; // Skill IDs provided
name?: string; // Human-readable name
description?: string; // Plugin description
version?: string; // Plugin version
uiHints?: Record<string, PluginConfigUiHint>; // UI config hints
contracts?: PluginManifestContracts; // Capability ownership
channelConfigs?: Record<string, PluginManifestChannelConfig>; // Channel configs
};
This manifest structure enables manifest-first behavior—the system can understand plugin capabilities, validate config, and make wiring decisions without loading the plugin runtime. This is why openclaw doctor can report on plugin status before any plugins are activated.
Three Plugin Types
OpenClaw plugins fall into three categories, each with distinct contracts:
-
Channel plugins: Implement messaging platform integrations
- Contract:
src/channels/plugins/types.plugin.tsdefines inbound/outbound handlers, pairing logic, and config schema - Examples: Telegram, Discord, Slack, Signal, iMessage, WhatsApp, Matrix, Zalo
- Boundary: Channels use the Plugin SDK's channel contract; they don't import other channels or core agent logic
- Contract:
-
Provider plugins: Implement model provider integrations
- Contract:
src/plugin-sdk/provider-entry.tsdefines auth, model catalog, and inference hooks - Examples: OpenAI, Anthropic, Google, xAI, Moonshot
- Boundary: Providers register models through SDK hooks; core doesn't read
plugins.entries.<id>.configdirectly
- Contract:
-
Skill plugins: Implement agent capabilities (often packaged as "apps" in the plugin ecosystem)
- Contract:
openclaw/plugin-sdk/core.tsdefines tool registration and skill metadata - Examples: Memory plugins, media understanding, web search, custom tools
- Boundary: Skills register tools through the Gateway; they don't reach into agent internals
- Contract:
☕ 茶水间时间:Why 95+ bundled plugins? OpenClaw includes popular channels and providers as "bundled workspace plugins" in the
extensions/directory. This serves two purposes: (1) users get common integrations out-of-the-box without hunting for community plugins, and (2) these plugins serve as reference implementations for third-party developers. The bundled plugins follow the same Plugin SDK contract as any external plugin—they're not "special" internally, just conveniently packaged.
Boundary Enforcement
The architecture enforces boundaries through multiple mechanisms:
- Import restrictions: Extensions cannot import
src/**internals (enforced inCLAUDE.md) - SDK facades: Public contracts are exported through
openclaw/plugin-sdk/*barrels - Manifest validation: Plugin capabilities must be declared in manifests before runtime
- Contract tests: Automated tests verify plugins respect their contracts
When core needs a new plugin seam, it's added to the Plugin SDK first—not as an ad hoc import. This backward-compatible contract evolution ensures existing plugins continue working as the system evolves.
Data Flow Patterns
Understanding component boundaries is one thing; seeing how data flows through them is another. Let's trace two critical flows with concrete file paths and function names.
CLI Command Execution Flow
When you run openclaw agent "explain quantum computing", the execution path is:
-
Entry wrapper (
openclaw.mjs:1-181): Checks Node version, enables compile cache, loadsdist/entry.js -
Main entry (
src/entry.ts:102-156):runMainOrRootHelp()normalizes argv, handles profile/container arguments -
CLI routing (
src/cli/run-main.ts):tryRouteCli()parses the command path, routes to agent command handler -
Gateway client (
src/gateway/client.ts): CreatesGatewayClient, connects to WebSocket atws://127.0.0.1:18789 -
Protocol handshake (
src/gateway/protocol/schema/types.ts:7-11): SendsConnectParamswith auth tokens, receivesHelloOkwith negotiated protocol version -
Session creation: Sends
RequestFramewith methodsessionsCreate, Gateway creates or resolves session -
Message send: Sends
RequestFramewith methodsessionsSendcontaining the user's message -
Agent invocation (
src/agents/): Agent runtime receives message, invokes Pi agent with context -
Streaming response: Agent streams response through Gateway
EventFramemessages with typeagentChunk -
CLI output: CLI client receives events, prints streaming response to terminal
This flow demonstrates that the CLI is a thin client—it doesn't execute agent logic, manage state, or implement protocols. It translates human commands into Gateway protocol calls and renders Gateway events as terminal output.
Inbound Message Handling Flow
When a Telegram message arrives, the path to agent response is more complex:
-
Platform webhook (
extensions/telegram/): Telegram sends webhook to OpenClaw's HTTP endpoint -
Channel inbound (
src/channels/telegram/inbound.ts): Channel receives webhook payload, validates signature -
Envelope creation (
src/channels/plugins/types.core.ts): Wraps message inInboundEnvelopewith metadata (channelId, accountId, peer, message type, content) -
Routing (
src/routing/): Routes envelope to appropriate session based on channel/account/peer -
Session resolution (
src/sessions/): Resolves or creates session, updates session state -
Agent submission (
src/agents/): Submits message to Pi agent with session context, available tools -
Tool invocation (
src/gateway/tools-invoke-http.ts): Agent requests tool call, Gateway routes to appropriate plugin -
Tool execution: Provider/skill plugin executes tool, returns result through Gateway
-
Agent streaming: Agent streams response chunks through Gateway events
-
Channel outbound (
src/channels/telegram/send.ts): Channel receives agent reply, formats and sends via Telegram API
This flow shows how channels are pure adapters—they translate platform protocols to OpenClaw's envelope format and back. The agent doesn't know about Telegram; it just receives a message with context and responds. The Gateway coordinates the handoff between channel, session, agent, and tool execution.
The InboundEnvelope carries everything needed for routing and policy:
// Conceptual structure (simplified)
type InboundEnvelope = {
channelId: string; // "telegram"
accountId: string; // Telegram bot ID
peer: { // Sender info
id: string; // User ID
username?: string; // Optional handle
displayName: string; // Display name
};
messageType: "dm" | "group"; // Direct or group message
content: string; // Message text
metadata: { // Platform-specific metadata
messageId: string;
replyTo?: string; // If this is a reply
attachments?: Array<Attachment>;
};
};
🤿 深入底层:The routing logic in
src/routing/determines whether a message creates a new session or resumes an existing one. For DMs, this is straightforward (one session per peer). For groups, it's more complex—groups can share sessions or have isolated sessions depending on configuration. The routing decision respects channel pairing policies and group access controls before resolving a session.
Boundary Enforcement in Action
Notice what doesn't happen in these flows:
- The CLI doesn't import
src/agents/directly—it goes through the Gateway protocol - Channels don't import
src/agents/—they submit envelopes and receive replies - Provider plugins don't import core agent logic—they register tools and model catalogs
- The Agent runtime doesn't import channel code—it receives normalized messages through sessions
Each component stays in its lane, communicating through defined contracts. This is why OpenClaw can add new channels, new providers, and new agent capabilities without rewriting core systems.
With the architecture mapped out—the Gateway control plane at the center, components with clear responsibilities, plugins extending through contracts, and data flowing through boundaries—we're ready to get OpenClaw running on your local machine. The next chapter moves from architecture to action: installing OpenClaw, configuring your first model, and exploring its capabilities hands-on.
Chapter Exercises
-
Trace the command flow: Pick a CLI command (e.g.,
openclaw config get) and trace it throughsrc/cli/run-main.ts→src/gateway/client.ts→ Gateway protocol → handler. Identify which files handle each step. -
Map a message flow: Starting from a Telegram webhook, follow the path through channel inbound → envelope → routing → session → agent → response → channel outbound. Draw a diagram showing which component owns each step.
-
Find boundary enforcement: Locate three places in the codebase where the architecture enforces a boundary (hint: check
CLAUDE.mdfor import restrictions, look for SDK exports insrc/plugin-sdk/, find manifest validation insrc/plugins/). Explain what each boundary protects and how it's enforced.