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
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` - Provider: `ollama`
- Auth: None required (local server) - 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
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` - Provider: `vllm`
- Auth: Optional (depends on your server) - 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. 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.) ### Local proxies (LM Studio, vLLM, LiteLLM, etc.)
Example (OpenAIcompatible): 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. See [Voice Call](/plugins/voice-call) for a concrete example plugin.
Looking for third-party listings? See [Community plugins](/plugins/community). 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) ## Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install `@openclaw/msteams` if you use Teams. - 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. Plugins run **inprocess** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools). 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 ## Runtime helpers
Plugins can access selected core helpers via `api.runtime`. For telephony TTS: 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. 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: Hardening notes:
- If `plugins.allow` is empty and non-bundled plugins are discoverable, OpenClaw logs a startup warning with plugin ids and sources. - 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 If multiple plugins resolve to the same id, the first match in the order above
wins and lower-precedence copies are ignored. 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 ### Package packs
A plugin directory may include a `package.json` with `openclaw.extensions`: 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 If a plugin exports `id`, OpenClaw uses it but warns when it doesnt match the
configured id. 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 ## Config
```json5 ```json5
@ -390,6 +525,17 @@ Validation rules (strict):
`openclaw.plugin.json` (`configSchema`). `openclaw.plugin.json` (`configSchema`).
- If a plugin is disabled, its config is preserved and a **warning** is emitted. - 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) ## Plugin slots (exclusive categories)
Some plugin categories are **exclusive** (only one active at a time). Use Some plugin categories are **exclusive** (only one active at a time). Use
@ -488,6 +634,19 @@ Plugins export either:
- A function: `(api) => { ... }` - A function: `(api) => { ... }`
- An object: `{ id, name, configSchema, register(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: Context engine plugins can also register a runtime-owned context manager:
```ts ```ts
@ -603,13 +762,150 @@ Migration guidance:
## Provider plugins (model auth) ## Provider plugins (model auth)
Plugins can register **model provider auth** flows so users can run OAuth or Plugins can register **model providers** so users can run OAuth or API-key
API-key setup inside OpenClaw (no external scripts needed). 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 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 models auth login --provider <id> [--method <id>]`
- `openclaw onboard`
- model-picker “custom provider” setup entries
- implicit provider discovery during model resolution/listing
Example: 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. `openUrl`, and `oauth.createVpsAwareHandlers` helpers.
- Return `configPatch` when you need to add default models or provider config. - Return `configPatch` when you need to add default models or provider config.
- Return `defaultModel` so `--set-default` can update agent defaults. - 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 ### Register a messaging channel