docs: explain plugin architecture

This commit is contained in:
Peter Steinberger 2026-03-12 22:24:28 +00:00
parent d83491e751
commit 319766639a
3 changed files with 471 additions and 6 deletions

View File

@ -352,7 +352,7 @@ See [/providers/minimax](/providers/minimax) for setup details, model options, a
### Ollama
Ollama is a local LLM runtime that provides an OpenAI-compatible API:
Ollama ships as a bundled provider plugin and uses Ollama's native API:
- Provider: `ollama`
- Auth: None required (local server)
@ -372,11 +372,15 @@ ollama pull llama3.3
}
```
Ollama is detected locally at `http://127.0.0.1:11434` when you opt in with `OLLAMA_API_KEY`, and `openclaw onboard` can configure it directly as a first-class provider. See [/providers/ollama](/providers/ollama) for onboarding, cloud/local mode, and custom configuration.
Ollama is detected locally at `http://127.0.0.1:11434` when you opt in with
`OLLAMA_API_KEY`, and the bundled provider plugin adds Ollama directly to
`openclaw onboard` and the model picker. See [/providers/ollama](/providers/ollama)
for onboarding, cloud/local mode, and custom configuration.
### vLLM
vLLM is a local (or self-hosted) OpenAI-compatible server:
vLLM ships as a bundled provider plugin for local/self-hosted OpenAI-compatible
servers:
- Provider: `vllm`
- Auth: Optional (depends on your server)
@ -400,6 +404,34 @@ Then set a model (replace with one of the IDs returned by `/v1/models`):
See [/providers/vllm](/providers/vllm) for details.
### SGLang
SGLang ships as a bundled provider plugin for fast self-hosted
OpenAI-compatible servers:
- Provider: `sglang`
- Auth: Optional (depends on your server)
- Default base URL: `http://127.0.0.1:30000/v1`
To opt in to auto-discovery locally (any value works if your server does not
enforce auth):
```bash
export SGLANG_API_KEY="sglang-local"
```
Then set a model (replace with one of the IDs returned by `/v1/models`):
```json5
{
agents: {
defaults: { model: { primary: "sglang/your-model-id" } },
},
}
```
See [/providers/sglang](/providers/sglang) for details.
### Local proxies (LM Studio, vLLM, LiteLLM, etc.)
Example (OpenAIcompatible):

104
docs/providers/sglang.md Normal file
View File

@ -0,0 +1,104 @@
---
summary: "Run OpenClaw with SGLang (OpenAI-compatible self-hosted server)"
read_when:
- You want to run OpenClaw against a local SGLang server
- You want OpenAI-compatible /v1 endpoints with your own models
title: "SGLang"
---
# SGLang
SGLang can serve open-source models via an **OpenAI-compatible** HTTP API.
OpenClaw can connect to SGLang using the `openai-completions` API.
OpenClaw can also **auto-discover** available models from SGLang when you opt
in with `SGLANG_API_KEY` (any value works if your server does not enforce auth)
and you do not define an explicit `models.providers.sglang` entry.
## Quick start
1. Start SGLang with an OpenAI-compatible server.
Your base URL should expose `/v1` endpoints (for example `/v1/models`,
`/v1/chat/completions`). SGLang commonly runs on:
- `http://127.0.0.1:30000/v1`
2. Opt in (any value works if no auth is configured):
```bash
export SGLANG_API_KEY="sglang-local"
```
3. Run onboarding and choose `SGLang`, or set a model directly:
```bash
openclaw onboard
```
```json5
{
agents: {
defaults: {
model: { primary: "sglang/your-model-id" },
},
},
}
```
## Model discovery (implicit provider)
When `SGLANG_API_KEY` is set (or an auth profile exists) and you **do not**
define `models.providers.sglang`, OpenClaw will query:
- `GET http://127.0.0.1:30000/v1/models`
and convert the returned IDs into model entries.
If you set `models.providers.sglang` explicitly, auto-discovery is skipped and
you must define models manually.
## Explicit configuration (manual models)
Use explicit config when:
- SGLang runs on a different host/port.
- You want to pin `contextWindow`/`maxTokens` values.
- Your server requires a real API key (or you want to control headers).
```json5
{
models: {
providers: {
sglang: {
baseUrl: "http://127.0.0.1:30000/v1",
apiKey: "${SGLANG_API_KEY}",
api: "openai-completions",
models: [
{
id: "your-model-id",
name: "Local SGLang Model",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
},
],
},
},
},
}
```
## Troubleshooting
- Check the server is reachable:
```bash
curl http://127.0.0.1:30000/v1/models
```
- If requests fail with auth errors, set a real `SGLANG_API_KEY` that matches
your server configuration, or configure the provider explicitly under
`models.providers.sglang`.

View File

@ -43,6 +43,48 @@ prerelease tag such as `@beta`/`@rc` or an exact prerelease version.
See [Voice Call](/plugins/voice-call) for a concrete example plugin.
Looking for third-party listings? See [Community plugins](/plugins/community).
## Architecture
OpenClaw's plugin system has four layers:
1. **Manifest + discovery**
OpenClaw finds candidate plugins from configured paths, workspace roots,
global extension roots, and bundled extensions. Discovery reads
`openclaw.plugin.json` plus package metadata first.
2. **Enablement + validation**
Core decides whether a discovered plugin is enabled, disabled, blocked, or
selected for an exclusive slot such as memory.
3. **Runtime loading**
Enabled plugins are loaded in-process via jiti and register capabilities into
a central registry.
4. **Surface consumption**
The rest of OpenClaw reads the registry to expose tools, channels, provider
setup, hooks, HTTP routes, CLI commands, and services.
The important design boundary:
- discovery + config validation should work from **manifest/schema metadata**
without executing plugin code
- runtime behavior comes from the plugin module's `register(api)` path
That split lets OpenClaw validate config, explain missing/disabled plugins, and
build UI/schema hints before the full runtime is active.
## Execution model
Plugins run **in-process** with the Gateway. They are not sandboxed. A loaded
plugin has the same process-level trust boundary as core code.
Implications:
- a plugin can register tools, network handlers, hooks, and services
- a plugin bug can crash or destabilize the gateway
- a malicious plugin is equivalent to arbitrary code execution inside the
OpenClaw process
Use allowlists and explicit install/load paths for non-bundled plugins. Treat
workspace plugins as development-time code, not production defaults.
## Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install `@openclaw/msteams` if you use Teams.
@ -78,6 +120,48 @@ Plugins can register:
Plugins run **inprocess** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
## Load pipeline
At startup, OpenClaw does roughly this:
1. discover candidate plugin roots
2. read `openclaw.plugin.json` and package metadata
3. reject unsafe candidates
4. normalize plugin config (`plugins.enabled`, `allow`, `deny`, `entries`,
`slots`, `load.paths`)
5. decide enablement for each candidate
6. load enabled modules via jiti
7. call `register(api)` and collect registrations into the plugin registry
8. expose the registry to commands/runtime surfaces
The safety gates happen **before** runtime execution. Candidates are blocked
when the entry escapes the plugin root, the path is world-writable, or path
ownership looks suspicious for non-bundled plugins.
### Manifest-first behavior
The manifest is the control-plane source of truth. OpenClaw uses it to:
- identify the plugin
- discover declared channels/skills/config schema
- validate `plugins.entries.<id>.config`
- augment Control UI labels/placeholders
- show install/catalog metadata
The runtime module is the data-plane part. It registers actual behavior such as
hooks, tools, commands, or provider flows.
### What the loader caches
OpenClaw keeps short in-process caches for:
- discovery results
- manifest registry data
- loaded plugin registries
These caches reduce bursty startup and repeated command overhead. They are safe
to think of as short-lived performance caches, not persistence.
## Runtime helpers
Plugins can access selected core helpers via `api.runtime`. For telephony TTS:
@ -259,6 +343,10 @@ Default-on bundled plugin exceptions:
Installed plugins are enabled by default, but can be disabled the same way.
Workspace plugins are **disabled by default** unless you explicitly enable them
or allowlist them. This is intentional: a checked-out repo should not silently
become production gateway code.
Hardening notes:
- If `plugins.allow` is empty and non-bundled plugins are discoverable, OpenClaw logs a startup warning with plugin ids and sources.
@ -275,6 +363,25 @@ manifest.
If multiple plugins resolve to the same id, the first match in the order above
wins and lower-precedence copies are ignored.
### Enablement rules
Enablement is resolved after discovery:
- `plugins.enabled: false` disables all plugins
- `plugins.deny` always wins
- `plugins.entries.<id>.enabled: false` disables that plugin
- workspace-origin plugins are disabled by default
- allowlists restrict the active set when `plugins.allow` is non-empty
- bundled plugins are disabled by default unless:
- the bundled id is in the built-in default-on set, or
- you explicitly enable it, or
- channel config implicitly enables the bundled channel plugin
- exclusive slots can force-enable the selected plugin for that slot
In current core, bundled default-on ids include local/provider helpers such as
`ollama`, `sglang`, `vllm`, plus `device-pair`, `phone-control`, and
`talk-voice`.
### Package packs
A plugin directory may include a `package.json` with `openclaw.extensions`:
@ -354,6 +461,34 @@ Default plugin ids:
If a plugin exports `id`, OpenClaw uses it but warns when it doesnt match the
configured id.
## Registry model
Loaded plugins do not directly mutate random core globals. They register into a
central plugin registry.
The registry tracks:
- plugin records (identity, source, origin, status, diagnostics)
- tools
- legacy hooks and typed hooks
- channels
- providers
- gateway RPC handlers
- HTTP routes
- CLI registrars
- background services
- plugin-owned commands
Core features then read from that registry instead of talking to plugin modules
directly. This keeps loading one-way:
- plugin module -> registry registration
- core runtime -> registry consumption
That separation matters for maintainability. It means most core surfaces only
need one integration point: "read the registry", not "special-case every plugin
module".
## Config
```json5
@ -390,6 +525,17 @@ Validation rules (strict):
`openclaw.plugin.json` (`configSchema`).
- If a plugin is disabled, its config is preserved and a **warning** is emitted.
### Disabled vs missing vs invalid
These states are intentionally different:
- **disabled**: plugin exists, but enablement rules turned it off
- **missing**: config references a plugin id that discovery did not find
- **invalid**: plugin exists, but its config does not match the declared schema
OpenClaw preserves config for disabled plugins so toggling them back on is not
destructive.
## Plugin slots (exclusive categories)
Some plugin categories are **exclusive** (only one active at a time). Use
@ -488,6 +634,19 @@ Plugins export either:
- A function: `(api) => { ... }`
- An object: `{ id, name, configSchema, register(api) { ... } }`
`register(api)` is where plugins attach behavior. Common registrations include:
- `registerTool`
- `registerHook`
- `on(...)` for typed lifecycle hooks
- `registerChannel`
- `registerProvider`
- `registerHttpRoute`
- `registerCommand`
- `registerCli`
- `registerContextEngine`
- `registerService`
Context engine plugins can also register a runtime-owned context manager:
```ts
@ -603,13 +762,150 @@ Migration guidance:
## Provider plugins (model auth)
Plugins can register **model provider auth** flows so users can run OAuth or
API-key setup inside OpenClaw (no external scripts needed).
Plugins can register **model providers** so users can run OAuth or API-key
setup inside OpenClaw, surface provider setup in onboarding/model-pickers, and
contribute implicit provider discovery.
Provider plugins are the modular extension seam for model-provider setup. They
are not just "OAuth helpers" anymore.
### Provider plugin lifecycle
A provider plugin can participate in four distinct phases:
1. **Auth**
`auth[].run(ctx)` performs OAuth, API-key capture, device code, or custom
setup and returns auth profiles plus optional config patches.
2. **Wizard integration**
`wizard.onboarding` adds an entry to `openclaw onboard`.
`wizard.modelPicker` adds a setup entry to the model picker.
3. **Implicit discovery**
`discovery.run(ctx)` can contribute provider config automatically during
model resolution/listing.
4. **Post-selection follow-up**
`onModelSelected(ctx)` runs after a model is chosen. Use this for provider-
specific work such as downloading a local model.
This is the recommended split because these phases have different lifecycle
requirements:
- auth is interactive and writes credentials/config
- wizard metadata is static and UI-facing
- discovery should be safe, quick, and failure-tolerant
- post-select hooks are side effects tied to the chosen model
### Provider auth contract
`auth[].run(ctx)` returns:
- `profiles`: auth profiles to write
- `configPatch`: optional `openclaw.json` changes
- `defaultModel`: optional `provider/model` ref
- `notes`: optional user-facing notes
Core then:
1. writes the returned auth profiles
2. applies auth-profile config wiring
3. merges the config patch
4. optionally applies the default model
5. runs the provider's `onModelSelected` hook when appropriate
That means a provider plugin owns the provider-specific setup logic, while core
owns the generic persistence and config-merge path.
### Provider wizard metadata
`wizard.onboarding` controls how the provider appears in grouped onboarding:
- `choiceId`: auth-choice value
- `choiceLabel`: option label
- `choiceHint`: short hint
- `groupId`: group bucket id
- `groupLabel`: group label
- `groupHint`: group hint
- `methodId`: auth method to run
`wizard.modelPicker` controls how a provider appears as a "set this up now"
entry in model selection:
- `label`
- `hint`
- `methodId`
When a provider has multiple auth methods, the wizard can either point at one
explicit method or let OpenClaw synthesize per-method choices.
### Provider discovery contract
`discovery.run(ctx)` returns one of:
- `{ provider }`
- `{ providers }`
- `null`
Use `{ provider }` for the common case where the plugin owns one provider id.
Use `{ providers }` when a plugin discovers multiple provider entries.
The discovery context includes:
- the current config
- agent/workspace dirs
- process env
- a helper to resolve the provider API key and a discovery-safe API key value
Discovery should be:
- fast
- best-effort
- safe to skip on failure
- careful about side effects
It should not depend on prompts or long-running setup.
### Discovery ordering
Provider discovery runs in ordered phases:
- `simple`
- `profile`
- `paired`
- `late`
Use:
- `simple` for cheap environment-only discovery
- `profile` when discovery depends on auth profiles
- `paired` for providers that need to coordinate with another discovery step
- `late` for expensive or local-network probing
Most self-hosted providers should use `late`.
### Good provider-plugin boundaries
Good fit for provider plugins:
- local/self-hosted providers with custom setup flows
- provider-specific OAuth/device-code login
- implicit discovery of local model servers
- post-selection side effects such as model pulls
Less compelling fit:
- trivial API-key-only providers that differ only by env var, base URL, and one
default model
Those can still become plugins, but the main modularity payoff comes from
extracting behavior-rich providers first.
Register a provider via `api.registerProvider(...)`. Each provider exposes one
or more auth methods (OAuth, API key, device code, etc.). These methods power:
or more auth methods (OAuth, API key, device code, etc.). Those methods can
power:
- `openclaw models auth login --provider <id> [--method <id>]`
- `openclaw onboard`
- model-picker “custom provider” setup entries
- implicit provider discovery during model resolution/listing
Example:
@ -642,6 +938,31 @@ api.registerProvider({
},
},
],
wizard: {
onboarding: {
choiceId: "acme",
choiceLabel: "AcmeAI",
groupId: "acme",
groupLabel: "AcmeAI",
methodId: "oauth",
},
modelPicker: {
label: "AcmeAI (custom)",
hint: "Connect a self-hosted AcmeAI endpoint",
methodId: "oauth",
},
},
discovery: {
order: "late",
run: async () => ({
provider: {
baseUrl: "https://acme.example/v1",
api: "openai-completions",
apiKey: "${ACME_API_KEY}",
models: [],
},
}),
},
});
```
@ -651,6 +972,14 @@ Notes:
`openUrl`, and `oauth.createVpsAwareHandlers` helpers.
- Return `configPatch` when you need to add default models or provider config.
- Return `defaultModel` so `--set-default` can update agent defaults.
- `wizard.onboarding` adds a provider choice to `openclaw onboard`.
- `wizard.modelPicker` adds a “setup this provider” entry to the model picker.
- `discovery.run` returns either `{ provider }` for the plugins own provider id
or `{ providers }` for multi-provider discovery.
- `discovery.order` controls when the provider runs relative to built-in
discovery phases: `simple`, `profile`, `paired`, or `late`.
- `onModelSelected` is the post-selection hook for provider-specific follow-up
work such as pulling a local model.
### Register a messaging channel