docs(plugin-sdk): document public SDK surface

This commit is contained in:
Peter Steinberger
2026-03-22 08:50:43 -07:00
parent e1ff24903f
commit 05279539a8
34 changed files with 1260 additions and 16 deletions

View File

@@ -181,7 +181,7 @@ my-plugin/
// Correct: focused subpaths
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
// Wrong: monolithic root (lint will reject this)
import { ... } from "openclaw/plugin-sdk";
@@ -195,22 +195,23 @@ my-plugin/
| --- | --- |
| `plugin-sdk/plugin-entry` | Canonical `definePluginEntry` helper + provider/plugin entry types |
| `plugin-sdk/core` | Channel entry helpers, channel builders, and shared base types |
| `plugin-sdk/channel-setup` | Setup wizard adapters |
| `plugin-sdk/runtime-store` | Safe module-level runtime storage |
| `plugin-sdk/setup` | Shared setup-wizard helpers |
| `plugin-sdk/channel-setup` | Channel setup adapters |
| `plugin-sdk/channel-pairing` | DM pairing primitives |
| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring |
| `plugin-sdk/channel-config-schema` | Config schema builders |
| `plugin-sdk/channel-policy` | Group/DM policy helpers |
| `plugin-sdk/channel-actions` | Shared `message` tool schema helpers |
| `plugin-sdk/channel-contract` | Pure channel types |
| `plugin-sdk/secret-input` | Secret input parsing/helpers |
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
| `plugin-sdk/runtime-store` | Persistent plugin storage |
| `plugin-sdk/allow-from` | Allowlist resolution |
| `plugin-sdk/reply-payload` | Message reply types |
| `plugin-sdk/provider-oauth` | OAuth login + PKCE helpers |
| `plugin-sdk/provider-auth` | Provider auth and OAuth helpers |
| `plugin-sdk/provider-onboard` | Provider onboarding config patches |
| `plugin-sdk/provider-models` | Model catalog helpers |
| `plugin-sdk/testing` | Test utilities |
</Accordion>
Use the narrowest subpath that matches the job.
Use the narrowest subpath that matches the job. For the curated map and
examples, see [Plugin SDK Overview](/plugins/sdk-overview).
</Step>
@@ -266,7 +267,7 @@ my-plugin/
For unit tests, import test helpers from the testing surface:
```typescript
import { createTestRuntime } from "openclaw/plugin-sdk/testing";
import { createWindowsCmdShimFixture } from "openclaw/plugin-sdk/testing";
```
</Step>
@@ -370,6 +371,13 @@ patterns is strongly recommended.
## Related
- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from deprecated compat surfaces
- [Plugin SDK Overview](/plugins/sdk-overview) — public SDK map and subpath guidance
- [Plugin Entry Points](/plugins/sdk-entrypoints) — `definePluginEntry` and `defineChannelPluginEntry`
- [Plugin Runtime](/plugins/sdk-runtime) — injected runtime and runtime-store
- [Plugin Setup](/plugins/sdk-setup) — setup, channel setup, and secret input helpers
- [Channel Plugin SDK](/plugins/sdk-channel-plugins) — channel contracts and actions
- [Provider Plugin SDK](/plugins/sdk-provider-plugins) — provider auth, onboarding, and catalogs
- [Plugin SDK Testing](/plugins/sdk-testing) — public test helpers
- [Plugin Architecture](/plugins/architecture) — internals and capability model
- [Plugin Manifest](/plugins/manifest) — full manifest schema
- [Plugin Agent Tools](/plugins/building-plugins#registering-agent-tools) — adding agent tools in a plugin

View File

@@ -0,0 +1,161 @@
---
title: "Channel Plugin SDK"
sidebarTitle: "Channel Plugins"
summary: "Contracts and helpers for native messaging channel plugins, including actions, routing, pairing, and setup"
read_when:
- You are building a native channel plugin
- You need to implement the shared `message` tool for a channel
- You need pairing, setup, or routing helpers for a channel
---
# Channel Plugin SDK
Channel plugins use `defineChannelPluginEntry(...)` from
`openclaw/plugin-sdk/core` and implement the `ChannelPlugin` contract.
## Minimal channel entry
```ts
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { exampleChannelPlugin } from "./src/channel.js";
import { setExampleRuntime } from "./src/runtime.js";
export default defineChannelPluginEntry({
id: "example-channel",
name: "Example Channel",
description: "Example native channel plugin",
plugin: exampleChannelPlugin,
setRuntime: setExampleRuntime,
});
```
## `ChannelPlugin` shape
Important sections of the contract:
- `meta`: docs, labels, and picker metadata
- `capabilities`: replies, polls, reactions, threads, media, and chat types
- `config` and `configSchema`: account resolution and config parsing
- `setup` and `setupWizard`: onboarding/setup flow
- `security`: DM policy and allowlist behavior
- `messaging`: target parsing and outbound session routing
- `actions`: shared `message` tool discovery and execution
- `pairing`, `threading`, `status`, `lifecycle`, `groups`, `directory`
For pure types, import from `openclaw/plugin-sdk/channel-contract`.
## Shared `message` tool
Channel plugins own their channel-specific part of the shared `message` tool
through `ChannelMessageActionAdapter`.
```ts
import { Type } from "@sinclair/typebox";
import { createMessageToolButtonsSchema } from "openclaw/plugin-sdk/channel-actions";
export const exampleActions = {
describeMessageTool() {
return {
actions: ["send", "edit"],
capabilities: ["buttons"],
schema: {
visibility: "current-channel",
properties: {
buttons: createMessageToolButtonsSchema(),
threadId: Type.String(),
},
},
};
},
async handleAction(ctx) {
if (ctx.action === "send") {
return {
content: [{ type: "text", text: `send to ${String(ctx.params.to)}` }],
};
}
return {
content: [{ type: "text", text: `unsupported action: ${ctx.action}` }],
};
},
};
```
Key types:
- `ChannelMessageActionAdapter`
- `ChannelMessageActionContext`
- `ChannelMessageActionDiscoveryContext`
- `ChannelMessageToolDiscovery`
## Outbound routing helpers
When a channel plugin needs custom outbound routing, implement
`messaging.resolveOutboundSessionRoute(...)`.
Use `buildChannelOutboundSessionRoute(...)` from `plugin-sdk/core` to return the
standard route payload:
```ts
import { buildChannelOutboundSessionRoute } from "openclaw/plugin-sdk/core";
const messaging = {
resolveOutboundSessionRoute({ cfg, agentId, accountId, target }) {
return buildChannelOutboundSessionRoute({
cfg,
agentId,
channel: "example-channel",
accountId,
peer: { kind: "direct", id: target },
chatType: "direct",
from: accountId ?? "default",
to: target,
});
},
};
```
## Pairing helpers
Use `plugin-sdk/channel-pairing` for DM approval flows:
```ts
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
const pairing = createChannelPairingController({
core: runtime,
channel: "example-channel",
accountId: "default",
});
const result = pairing.issueChallenge({
agentId: "assistant",
requesterId: "user-123",
});
```
That surface also gives you scoped access to pairing storage helpers such as
allowlist reads and request upserts.
## Channel setup helpers
Use:
- `plugin-sdk/channel-setup` for optional or installable channels
- `plugin-sdk/setup` for setup adapters, DM policy, and allowlist prompts
- `plugin-sdk/webhook-ingress` for plugin-owned webhook routes
## Channel plugin guidance
- Keep transport-specific execution inside the channel package.
- Use `channel-contract` types in tests and local helpers.
- Keep `describeMessageTool(...)` and `handleAction(...)` aligned.
- Keep session routing in `messaging`, not in ad-hoc command handlers.
- Prefer focused subpaths over broad runtime coupling.
## Related
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Entry Points](/plugins/sdk-entrypoints)
- [Plugin Setup](/plugins/sdk-setup)
- [Plugin Internals](/plugins/architecture)

View File

@@ -0,0 +1,159 @@
---
title: "Plugin Entry Points"
sidebarTitle: "Entry Points"
summary: "How to define plugin entry files for provider, tool, channel, and setup plugins"
read_when:
- You are writing a plugin `index.ts`
- You need to choose between `definePluginEntry` and `defineChannelPluginEntry`
- You are adding a separate `setup-entry.ts`
---
# Plugin Entry Points
OpenClaw has two main entry helpers:
- `definePluginEntry(...)` for general plugins
- `defineChannelPluginEntry(...)` for native messaging channels
There is also `defineSetupPluginEntry(...)` for a separate setup-only module.
## `definePluginEntry(...)`
Use this for providers, tools, commands, services, memory plugins, and context
engines.
```ts
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "example-tools",
name: "Example Tools",
description: "Adds a command and a tool",
register(api: OpenClawPluginApi) {
api.registerCommand({
name: "example",
description: "Show plugin status",
handler: async () => ({ text: "example ok" }),
});
api.registerTool({
name: "example_lookup",
description: "Look up Example data",
parameters: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
async execute(_callId, params) {
return {
content: [{ type: "text", text: `lookup: ${String(params.query)}` }],
};
},
});
},
});
```
## `defineChannelPluginEntry(...)`
Use this for a plugin that registers a `ChannelPlugin`.
```ts
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { channelPlugin } from "./src/channel.js";
import { setRuntime } from "./src/runtime.js";
export default defineChannelPluginEntry({
id: "example-channel",
name: "Example Channel",
description: "Example messaging plugin",
plugin: channelPlugin,
setRuntime,
registerFull(api) {
api.registerTool({
name: "example_channel_status",
description: "Inspect Example Channel state",
parameters: { type: "object", properties: {} },
async execute() {
return { content: [{ type: "text", text: "ok" }] };
},
});
},
});
```
### Why `registerFull(...)` exists
OpenClaw can load plugins in setup-focused registration modes. `registerFull`
lets a channel plugin skip extra runtime-only registrations such as tools while
still registering the channel capability itself.
Use it for:
- agent tools
- gateway-only routes
- runtime-only commands
Do not use it for the actual `ChannelPlugin`; that belongs in `plugin: ...`.
## `defineSetupPluginEntry(...)`
Use this when a channel ships a second module for setup flows.
```ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { exampleSetupPlugin } from "./src/channel.setup.js";
export default defineSetupPluginEntry(exampleSetupPlugin);
```
This keeps the setup entry shape explicit and matches the bundled channel
pattern used in OpenClaw.
## One plugin, many capabilities
A single entry file can register multiple capabilities:
```ts
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "example-hybrid",
name: "Example Hybrid",
description: "Provider plus tools",
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "example",
label: "Example",
auth: [],
});
api.registerTool({
name: "example_ping",
description: "Simple health check",
parameters: { type: "object", properties: {} },
async execute() {
return { content: [{ type: "text", text: "pong" }] };
},
});
},
});
```
## Entry-file checklist
- Give the plugin a stable `id`.
- Keep `name` and `description` human-readable.
- Put schema at the entry level when the plugin has config.
- Register only public capabilities inside `register(api)`.
- Keep channel plugins on `plugin-sdk/core`.
- Keep non-channel plugins on `plugin-sdk/plugin-entry`.
## Related
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Runtime](/plugins/sdk-runtime)
- [Channel Plugin SDK](/plugins/sdk-channel-plugins)
- [Provider Plugin SDK](/plugins/sdk-provider-plugins)

View File

@@ -165,5 +165,6 @@ This is a temporary escape hatch, not a permanent solution.
## Related
- [Building Plugins](/plugins/building-plugins)
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Internals](/plugins/architecture)
- [Plugin Manifest](/plugins/manifest)

View File

@@ -0,0 +1,175 @@
---
title: "Plugin SDK Overview"
sidebarTitle: "SDK Overview"
summary: "How the OpenClaw plugin SDK is organized, which subpaths are stable, and how to choose the right import"
read_when:
- You are starting a new OpenClaw plugin
- You need to choose the right plugin-sdk subpath
- You are replacing deprecated compat imports
---
# Plugin SDK Overview
The OpenClaw plugin SDK is split into **small public subpaths** under
`openclaw/plugin-sdk/<subpath>`.
Use the narrowest import that matches the job. That keeps plugin dependencies
small, avoids circular imports, and makes it clear which contract you depend on.
## Rules first
- Use focused imports such as `openclaw/plugin-sdk/plugin-entry`.
- Do not import the root `openclaw/plugin-sdk` barrel in new code.
- Do not import `openclaw/extension-api` in new code.
- Do not import `src/**` from plugin packages.
- Inside a plugin package, route internal imports through local files such as
`./api.ts` or `./runtime-api.ts`, not through the published SDK path for that
same plugin.
## SDK map
| Job | Subpath | Next page |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Define plugin entry modules | `plugin-sdk/plugin-entry`, `plugin-sdk/core` | [Plugin Entry Points](/plugins/sdk-entrypoints) |
| Use injected runtime helpers | `plugin-sdk/runtime`, `plugin-sdk/runtime-store` | [Plugin Runtime](/plugins/sdk-runtime) |
| Build setup/configure flows | `plugin-sdk/setup`, `plugin-sdk/channel-setup`, `plugin-sdk/secret-input` | [Plugin Setup](/plugins/sdk-setup) |
| Build channel plugins | `plugin-sdk/core`, `plugin-sdk/channel-contract`, `plugin-sdk/channel-actions`, `plugin-sdk/channel-pairing` | [Channel Plugin SDK](/plugins/sdk-channel-plugins) |
| Build provider plugins | `plugin-sdk/plugin-entry`, `plugin-sdk/provider-auth`, `plugin-sdk/provider-onboard`, `plugin-sdk/provider-models`, `plugin-sdk/provider-usage` | [Provider Plugin SDK](/plugins/sdk-provider-plugins) |
| Test plugin code | `plugin-sdk/testing` | [Plugin SDK Testing](/plugins/sdk-testing) |
## Typical plugin layout
```text
my-plugin/
├── package.json
├── openclaw.plugin.json
├── index.ts
├── setup-entry.ts
├── api.ts
├── runtime-api.ts
└── src/
├── provider.ts
├── setup.ts
└── provider.test.ts
```
```ts
// api.ts
export {
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/plugin-entry";
```
## What belongs where
### Entry helpers
- `plugin-sdk/plugin-entry` is the default entry surface for providers, tools,
commands, services, memory plugins, and context engines.
- `plugin-sdk/core` adds channel-focused helpers such as
`defineChannelPluginEntry(...)`.
### Runtime helpers
- Use `api.runtime.*` for trusted in-process helpers that OpenClaw injects at
registration time.
- Use `plugin-sdk/runtime-store` when plugin modules need a mutable runtime slot
that is initialized later.
### Setup helpers
- `plugin-sdk/setup` contains shared setup-wizard helpers and config patch
helpers.
- `plugin-sdk/channel-setup` contains channel-specific setup adapters.
- `plugin-sdk/secret-input` exposes the shared secret-input schema helpers.
### Channel helpers
- `plugin-sdk/channel-contract` exports pure channel types.
- `plugin-sdk/channel-actions` covers shared `message` tool schema helpers.
- `plugin-sdk/channel-pairing` covers pairing approval flows.
- `plugin-sdk/webhook-ingress` covers plugin-owned webhook routes.
### Provider helpers
- `plugin-sdk/provider-auth` covers auth flows and credential helpers.
- `plugin-sdk/provider-onboard` covers config patches after auth/setup.
- `plugin-sdk/provider-models` covers catalog and model-definition helpers.
- `plugin-sdk/provider-usage` covers usage snapshot helpers.
- `plugin-sdk/provider-setup` and `plugin-sdk/self-hosted-provider-setup`
cover self-hosted and local-model onboarding.
## Example: mixing subpaths in one plugin
```ts
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { applyProviderConfigWithDefaultModel } from "openclaw/plugin-sdk/provider-onboard";
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
export default definePluginEntry({
id: "example-provider",
name: "Example Provider",
description: "Small provider plugin example",
configSchema: {
jsonSchema: {
type: "object",
additionalProperties: false,
properties: {
apiKey: { type: "string" },
},
},
safeParse(value) {
return buildSecretInputSchema().safeParse((value as { apiKey?: unknown })?.apiKey);
},
},
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "example",
label: "Example",
auth: [
createProviderApiKeyAuthMethod({
providerId: "example",
methodId: "api-key",
label: "Example API key",
optionKey: "exampleApiKey",
flagName: "--example-api-key",
envVar: "EXAMPLE_API_KEY",
promptMessage: "Enter Example API key",
profileId: "example:default",
defaultModel: "example/default",
applyConfig: (cfg) =>
applyProviderConfigWithDefaultModel(cfg, "example", {
id: "default",
name: "Default",
}),
}),
],
});
},
});
```
## Choose the smallest public seam
If a helper exists on a focused subpath, prefer that over a broader runtime
surface.
- Prefer `plugin-sdk/provider-auth` over reaching into unrelated provider files.
- Prefer `plugin-sdk/channel-contract` for types in tests and helper modules.
- Prefer `plugin-sdk/runtime-store` over custom mutable globals.
- Prefer `plugin-sdk/testing` for shared test fixtures.
## Related
- [Building Plugins](/plugins/building-plugins)
- [Plugin Entry Points](/plugins/sdk-entrypoints)
- [Plugin Runtime](/plugins/sdk-runtime)
- [Plugin Setup](/plugins/sdk-setup)
- [Channel Plugin SDK](/plugins/sdk-channel-plugins)
- [Provider Plugin SDK](/plugins/sdk-provider-plugins)
- [Plugin SDK Testing](/plugins/sdk-testing)
- [Plugin SDK Migration](/plugins/sdk-migration)

View File

@@ -0,0 +1,184 @@
---
title: "Provider Plugin SDK"
sidebarTitle: "Provider Plugins"
summary: "Contracts and helper subpaths for model-provider plugins, including auth, onboarding, catalogs, and usage"
read_when:
- You are building a model provider plugin
- You need auth helpers for API keys or OAuth
- You need onboarding config patches or catalog helpers
---
# Provider Plugin SDK
Provider plugins use `definePluginEntry(...)` and call `api.registerProvider(...)`
with a `ProviderPlugin` definition.
## Minimal provider entry
```ts
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "example-provider",
name: "Example Provider",
description: "Example text-inference provider plugin",
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "example",
label: "Example",
auth: [],
});
},
});
```
## Provider subpaths
| Subpath | Use it for |
| --------------------------------------- | ---------------------------------------------- |
| `plugin-sdk/provider-auth` | API key, OAuth, auth-profile, and PKCE helpers |
| `plugin-sdk/provider-onboard` | Config patches after setup/auth |
| `plugin-sdk/provider-models` | Model-definition and catalog helpers |
| `plugin-sdk/provider-setup` | Shared local/self-hosted setup flows |
| `plugin-sdk/self-hosted-provider-setup` | OpenAI-compatible self-hosted providers |
| `plugin-sdk/provider-usage` | Usage snapshot fetch helpers |
## API key auth
`createProviderApiKeyAuthMethod(...)` is the standard helper for API-key
providers:
```ts
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { applyProviderConfigWithDefaultModel } from "openclaw/plugin-sdk/provider-onboard";
const auth = [
createProviderApiKeyAuthMethod({
providerId: "example",
methodId: "api-key",
label: "Example API key",
optionKey: "exampleApiKey",
flagName: "--example-api-key",
envVar: "EXAMPLE_API_KEY",
promptMessage: "Enter Example API key",
profileId: "example:default",
defaultModel: "example/default",
applyConfig: (cfg) =>
applyProviderConfigWithDefaultModel(cfg, "example", {
id: "default",
name: "Default",
}),
}),
];
```
## OAuth auth
`buildOauthProviderAuthResult(...)` builds the standard auth result payload for
OAuth-style providers:
```ts
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
async function runOAuthLogin() {
return buildOauthProviderAuthResult({
providerId: "example-portal",
defaultModel: "example-portal/default",
access: "access-token",
refresh: "refresh-token",
email: "user@example.com",
notes: ["Tokens auto-refresh when the provider supports refresh tokens."],
});
}
```
## Catalog and discovery hooks
Provider plugins usually implement either `catalog` or the legacy `discovery`
alias. `catalog` is preferred.
```ts
api.registerProvider({
id: "example",
label: "Example",
auth,
catalog: {
order: "simple",
async run(ctx) {
const apiKey = ctx.resolveProviderApiKey("example").apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
api: "openai",
baseUrl: "https://api.example.com/v1",
apiKey,
models: [
{
id: "default",
name: "Default",
input: ["text"],
},
],
},
};
},
},
});
```
## Onboarding config patches
`plugin-sdk/provider-onboard` keeps post-auth config writes consistent.
Common helpers:
- `applyProviderConfigWithDefaultModel(...)`
- `applyProviderConfigWithDefaultModels(...)`
- `applyProviderConfigWithModelCatalog(...)`
- `applyAgentDefaultModelPrimary(...)`
- `ensureModelAllowlistEntry(...)`
## Self-hosted and local model setup
Use `plugin-sdk/provider-setup` or
`plugin-sdk/self-hosted-provider-setup` when the provider is an OpenAI-style
backend, Ollama, SGLang, or vLLM.
Examples from the shared setup surfaces:
- `promptAndConfigureOllama(...)`
- `configureOllamaNonInteractive(...)`
- `promptAndConfigureOpenAICompatibleSelfHostedProvider(...)`
- `discoverOpenAICompatibleSelfHostedProvider(...)`
These helpers keep setup behavior aligned with built-in provider flows.
## Usage snapshots
If the provider owns quota or usage endpoints, use `resolveUsageAuth(...)` and
`fetchUsageSnapshot(...)`.
`plugin-sdk/provider-usage` includes shared fetch helpers such as:
- `fetchClaudeUsage(...)`
- `fetchCodexUsage(...)`
- `fetchGeminiUsage(...)`
- `fetchMinimaxUsage(...)`
- `fetchZaiUsage(...)`
## Provider guidance
- Keep auth logic in `provider-auth`.
- Keep config mutation in `provider-onboard`.
- Keep catalog/model helpers in `provider-models`.
- Keep usage logic in `provider-usage`.
- Use `catalog`, not `discovery`, in new plugins.
## Related
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Entry Points](/plugins/sdk-entrypoints)
- [Plugin Setup](/plugins/sdk-setup)
- [Plugin Internals](/plugins/architecture#provider-runtime-hooks)

156
docs/plugins/sdk-runtime.md Normal file
View File

@@ -0,0 +1,156 @@
---
title: "Plugin Runtime"
sidebarTitle: "Runtime"
summary: "How `api.runtime` works, when to use it, and how to manage plugin runtime state safely"
read_when:
- You need to call runtime helpers from a plugin
- You are deciding between hooks and injected runtime
- You need a safe module-level runtime store
---
# Plugin Runtime
Native OpenClaw plugins receive a trusted runtime through `api.runtime`.
Use it for **host-owned operations** that should stay inside OpenClaws runtime:
- reading and writing config
- agent/session helpers
- system commands with OpenClaw timeouts
- media, speech, image-generation, and web-search runtime calls
- channel-owned helpers for bundled channel plugins
## When to use runtime vs focused SDK helpers
- Use focused SDK helpers when a public subpath already models the job.
- Use `api.runtime.*` when the host owns the operation or state.
- Prefer hooks for loose integrations that do not need tight in-process access.
## Runtime namespaces
| Namespace | What it covers |
| -------------------------------- | -------------------------------------------------- |
| `api.runtime.config` | Load and persist OpenClaw config |
| `api.runtime.agent` | Agent workspace, identity, timeouts, session store |
| `api.runtime.system` | System events, heartbeats, command execution |
| `api.runtime.media` | File/media loading and transforms |
| `api.runtime.tts` | Speech synthesis and voice listing |
| `api.runtime.mediaUnderstanding` | Image/audio/video understanding |
| `api.runtime.imageGeneration` | Image generation providers |
| `api.runtime.webSearch` | Runtime web-search execution |
| `api.runtime.modelAuth` | Resolve model/provider credentials |
| `api.runtime.subagent` | Spawn, wait, inspect, and delete subagent sessions |
| `api.runtime.channel` | Channel-heavy helpers for native channel plugins |
## Example: read and persist config
```ts
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "talk-settings",
name: "Talk Settings",
description: "Example runtime config write",
register(api: OpenClawPluginApi) {
api.registerCommand({
name: "talk-mode",
description: "Enable talk mode",
handler: async () => {
const cfg = api.runtime.config.loadConfig();
const nextConfig = {
...cfg,
talk: {
...cfg.talk,
enabled: true,
},
};
await api.runtime.config.writeConfigFile(nextConfig);
return { text: "talk mode enabled" };
},
});
},
});
```
## Example: use a runtime service owned by OpenClaw
```ts
const cfg = api.runtime.config.loadConfig();
const voices = await api.runtime.tts.listVoices({
provider: "openai",
cfg,
});
return {
text: voices.map((voice) => `${voice.name ?? voice.id}: ${voice.id}`).join("\n"),
};
```
## `createPluginRuntimeStore(...)`
Plugin modules often need a small mutable slot for runtime-backed helpers. Use
`plugin-sdk/runtime-store` instead of an unguarded `let runtime`.
```ts
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { channelPlugin } from "./src/channel.js";
const runtimeStore = createPluginRuntimeStore<{
logger: { info(message: string): void };
}>("Example Channel runtime not initialized");
export function setExampleRuntime(runtime: { logger: { info(message: string): void } }) {
runtimeStore.setRuntime(runtime);
}
export function getExampleRuntime() {
return runtimeStore.getRuntime();
}
export default defineChannelPluginEntry({
id: "example-channel",
name: "Example Channel",
description: "Example runtime store usage",
plugin: channelPlugin,
setRuntime: setExampleRuntime,
});
```
`createPluginRuntimeStore(...)` gives you:
- `setRuntime(next)`
- `clearRuntime()`
- `tryGetRuntime()`
- `getRuntime()`
`getRuntime()` throws with your custom message if the runtime was never set.
## Channel runtime note
`api.runtime.channel.*` is the heaviest namespace. It exists for native channel
plugins that need tight coupling with the OpenClaw messaging stack.
Prefer narrower subpaths such as:
- `plugin-sdk/channel-pairing`
- `plugin-sdk/channel-actions`
- `plugin-sdk/channel-feedback`
- `plugin-sdk/channel-lifecycle`
Use `api.runtime.channel.*` when the operation is clearly host-owned and there
is no smaller public seam.
## Runtime safety guidelines
- Do not cache config snapshots longer than needed.
- Prefer `createPluginRuntimeStore(...)` for shared module state.
- Keep runtime-backed code behind small local helpers.
- Avoid reaching into runtime namespaces you do not need.
## Related
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Entry Points](/plugins/sdk-entrypoints)
- [Plugin Setup](/plugins/sdk-setup)
- [Channel Plugin SDK](/plugins/sdk-channel-plugins)

132
docs/plugins/sdk-setup.md Normal file
View File

@@ -0,0 +1,132 @@
---
title: "Plugin Setup"
sidebarTitle: "Setup"
summary: "Shared setup-wizard helpers for channel plugins, provider plugins, and secret inputs"
read_when:
- You are building a setup or onboarding flow
- You need shared allowlist or DM policy setup helpers
- You need the shared secret-input schema
---
# Plugin Setup
OpenClaw exposes shared setup helpers so plugin setup flows behave like the
built-in ones.
Main subpaths:
- `openclaw/plugin-sdk/setup`
- `openclaw/plugin-sdk/channel-setup`
- `openclaw/plugin-sdk/secret-input`
## Channel setup helpers
Use `plugin-sdk/channel-setup` when a channel plugin needs the standard setup
adapter and setup wizard shapes.
### Optional channel plugins
If a channel is installable but not always present, use
`createOptionalChannelSetupSurface(...)`:
```ts
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
export const optionalExampleSetup = createOptionalChannelSetupSurface({
channel: "example",
label: "Example Channel",
npmSpec: "@openclaw/example-channel",
docsPath: "/channels/example",
});
```
That returns:
- `setupAdapter`
- `setupWizard`
Both surfaces produce a consistent “install this plugin first” experience.
## Shared setup helpers
`plugin-sdk/setup` re-exports the setup primitives used by bundled channels.
Common helpers:
- `applySetupAccountConfigPatch(...)`
- `createPatchedAccountSetupAdapter(...)`
- `createEnvPatchedAccountSetupAdapter(...)`
- `createTopLevelChannelDmPolicy(...)`
- `setSetupChannelEnabled(...)`
- `promptResolvedAllowFrom(...)`
- `promptSingleChannelSecretInput(...)`
### Example: patch channel config in setup
```ts
import {
DEFAULT_ACCOUNT_ID,
createPatchedAccountSetupAdapter,
setSetupChannelEnabled,
} from "openclaw/plugin-sdk/setup";
export const exampleSetupAdapter = createPatchedAccountSetupAdapter({
resolveAccountId: ({ accountId }) => accountId ?? DEFAULT_ACCOUNT_ID,
applyPatch: ({ nextConfig, accountId }) => {
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
return setSetupChannelEnabled({
nextConfig,
channel: "example",
accountId: resolvedAccountId,
enabled: true,
});
},
});
```
## Secret input schema
Use `plugin-sdk/secret-input` instead of rolling your own secret-input parser.
```ts
import {
buildOptionalSecretInputSchema,
buildSecretInputArraySchema,
buildSecretInputSchema,
hasConfiguredSecretInput,
} from "openclaw/plugin-sdk/secret-input";
const ApiKeySchema = buildSecretInputSchema();
const OptionalApiKeySchema = buildOptionalSecretInputSchema();
const ExtraKeysSchema = buildSecretInputArraySchema();
const parsed = OptionalApiKeySchema.safeParse(process.env.EXAMPLE_API_KEY);
if (parsed.success && hasConfiguredSecretInput(parsed.data)) {
// ...
}
```
## Provider setup note
Provider-specific onboarding helpers live on provider-focused subpaths:
- `plugin-sdk/provider-auth`
- `plugin-sdk/provider-onboard`
- `plugin-sdk/provider-setup`
- `plugin-sdk/self-hosted-provider-setup`
See [Provider Plugin SDK](/plugins/sdk-provider-plugins).
## Setup guidance
- Keep setup input schemas strict and small.
- Reuse OpenClaws allowlist, DM-policy, and secret-input helpers.
- Keep setup-entry modules thin; move behavior into `src/`.
- Link docs from setup flows when install or auth steps are manual.
## Related
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Entry Points](/plugins/sdk-entrypoints)
- [Provider Plugin SDK](/plugins/sdk-provider-plugins)
- [Plugin Manifest](/plugins/manifest)

112
docs/plugins/sdk-testing.md Normal file
View File

@@ -0,0 +1,112 @@
---
title: "Plugin SDK Testing"
sidebarTitle: "Testing"
summary: "How to test plugin code with the public testing helpers and small local test doubles"
read_when:
- You are writing tests for a plugin
- You need fixtures for Windows command shims or shared routing failures
- You want to know what the public testing surface includes
---
# Plugin SDK Testing
OpenClaw keeps the public testing surface intentionally small.
Use `openclaw/plugin-sdk/testing` for helpers that are stable enough to support
for plugin authors, and build small plugin-local doubles for everything else.
## Public testing helpers
Current helpers include:
- `createWindowsCmdShimFixture(...)`
- `installCommonResolveTargetErrorCases(...)`
- `shouldAckReaction(...)`
- `removeAckReactionAfterReply(...)`
The testing surface also re-exports some shared types:
- `OpenClawConfig`
- `PluginRuntime`
- `RuntimeEnv`
- `ChannelAccountSnapshot`
- `ChannelGatewayContext`
## Example: Windows command shim fixture
```ts
import { createWindowsCmdShimFixture } from "openclaw/plugin-sdk/testing";
import { describe, expect, it } from "vitest";
describe("example CLI integration", () => {
it("creates a command shim", async () => {
await createWindowsCmdShimFixture({
shimPath: "/tmp/example.cmd",
scriptPath: "/tmp/example.js",
shimLine: 'node "%~dp0\\example.js" %*',
});
expect(true).toBe(true);
});
});
```
## Example: shared target-resolution failures
```ts
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
installCommonResolveTargetErrorCases({
implicitAllowFrom: ["user-1"],
resolveTarget({ to, mode, allowFrom }) {
if (!to?.trim()) {
return { ok: false, error: new Error("missing target") };
}
if (mode === "implicit" && allowFrom.length > 0 && to === "invalid-target") {
return { ok: false, error: new Error("invalid target") };
}
return { ok: true, to };
},
});
```
## Runtime doubles
There is no catch-all `createTestRuntime()` export on the public SDK today.
Instead:
- use the public testing helpers where they fit
- use `plugin-sdk/runtime` for small runtime adapters
- build tiny plugin-local runtime doubles for the rest
Example:
```ts
import { createLoggerBackedRuntime } from "openclaw/plugin-sdk/runtime";
const logs: string[] = [];
const runtime = createLoggerBackedRuntime({
logger: {
info(message) {
logs.push(`info:${message}`);
},
error(message) {
logs.push(`error:${message}`);
},
},
});
```
## Test guidance
- Prefer focused unit tests over giant end-to-end harnesses.
- Import pure types from focused SDK subpaths in tests.
- Keep plugin-local test doubles small and explicit.
- Avoid depending on non-exported OpenClaw test internals.
## Related
- [Building Plugins](/plugins/building-plugins)
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Runtime](/plugins/sdk-runtime)