diff --git a/docs/.i18n/glossary.zh-CN.json b/docs/.i18n/glossary.zh-CN.json index d1b1f3f3058..aee5598e8a1 100644 --- a/docs/.i18n/glossary.zh-CN.json +++ b/docs/.i18n/glossary.zh-CN.json @@ -238,5 +238,65 @@ { "source": "env var", "target": "环境变量" + }, + { + "source": "Plugin SDK", + "target": "插件 SDK" + }, + { + "source": "Plugin SDK Overview", + "target": "插件 SDK 概览" + }, + { + "source": "SDK Overview", + "target": "SDK 概览" + }, + { + "source": "Plugin Entry Points", + "target": "插件入口点" + }, + { + "source": "Entry Points", + "target": "入口点" + }, + { + "source": "Plugin Runtime", + "target": "插件运行时" + }, + { + "source": "Runtime", + "target": "运行时" + }, + { + "source": "Plugin Setup", + "target": "插件设置" + }, + { + "source": "Setup", + "target": "设置" + }, + { + "source": "Channel Plugin SDK", + "target": "渠道插件 SDK" + }, + { + "source": "Channel Plugins", + "target": "渠道插件" + }, + { + "source": "Provider Plugin SDK", + "target": "提供商插件 SDK" + }, + { + "source": "Provider Plugins", + "target": "提供商插件" + }, + { + "source": "Plugin SDK Testing", + "target": "插件 SDK 测试" + }, + { + "source": "Testing", + "target": "测试" } ] diff --git a/docs/docs.json b/docs/docs.json index be9fa476ea7..a176d2a180a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1045,6 +1045,18 @@ "plugins/architecture" ] }, + { + "group": "Plugin SDK", + "pages": [ + "plugins/sdk-overview", + "plugins/sdk-entrypoints", + "plugins/sdk-runtime", + "plugins/sdk-setup", + "plugins/sdk-channel-plugins", + "plugins/sdk-provider-plugins", + "plugins/sdk-testing" + ] + }, { "group": "Skills", "pages": [ diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index fa298294943..7e645c2079f 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -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 | - 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). @@ -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"; ``` @@ -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 diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md new file mode 100644 index 00000000000..b185d6df2af --- /dev/null +++ b/docs/plugins/sdk-channel-plugins.md @@ -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) diff --git a/docs/plugins/sdk-entrypoints.md b/docs/plugins/sdk-entrypoints.md new file mode 100644 index 00000000000..be3f36ef62c --- /dev/null +++ b/docs/plugins/sdk-entrypoints.md @@ -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) diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index b0cd34fda21..a5b69049b55 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -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) diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md new file mode 100644 index 00000000000..a3c6faf468c --- /dev/null +++ b/docs/plugins/sdk-overview.md @@ -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/`. + +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) diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md new file mode 100644 index 00000000000..d54564b373f --- /dev/null +++ b/docs/plugins/sdk-provider-plugins.md @@ -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) diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md new file mode 100644 index 00000000000..09d741fce39 --- /dev/null +++ b/docs/plugins/sdk-runtime.md @@ -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 OpenClaw’s 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) diff --git a/docs/plugins/sdk-setup.md b/docs/plugins/sdk-setup.md new file mode 100644 index 00000000000..21df88117ab --- /dev/null +++ b/docs/plugins/sdk-setup.md @@ -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 OpenClaw’s 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) diff --git a/docs/plugins/sdk-testing.md b/docs/plugins/sdk-testing.md new file mode 100644 index 00000000000..90373eca25d --- /dev/null +++ b/docs/plugins/sdk-testing.md @@ -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) diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 3ede326f0aa..06f9f5a9a2c 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -56,6 +56,9 @@ OpenClaw recognizes two plugin formats: Both show up under `openclaw plugins list`. See [Plugin Bundles](/plugins/bundles) for bundle details. +If you are writing a native plugin, start with [Building Plugins](/plugins/building-plugins) +and the [Plugin SDK Overview](/plugins/sdk-overview). + ## Official plugins ### Installable (npm) diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index f7275d81ed2..6d324e673ca 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -14,10 +14,12 @@ export type ChannelId = ChatChannelId | (string & {}); export type ChannelOutboundTargetMode = "explicit" | "implicit" | "heartbeat"; +/** Agent tool registered by a channel plugin. */ export type ChannelAgentTool = AgentTool & { ownerOnly?: boolean; }; +/** Lazy agent-tool factory used when tool availability depends on config. */ export type ChannelAgentToolFactory = (params: { cfg?: OpenClawConfig }) => ChannelAgentTool[]; /** @@ -57,6 +59,7 @@ export type ChannelMessageToolDiscovery = { schema?: ChannelMessageToolSchemaContribution | ChannelMessageToolSchemaContribution[] | null; }; +/** Shared setup input bag used by CLI, onboarding, and setup adapters. */ export type ChannelSetupInput = { name?: string; token?: string; @@ -115,6 +118,7 @@ export type ChannelHeartbeatDeps = { hasActiveWebListener?: () => boolean; }; +/** User-facing metadata used in docs, pickers, and setup surfaces. */ export type ChannelMeta = { id: ChannelId; label: string; @@ -136,6 +140,7 @@ export type ChannelMeta = { preferOver?: string[]; }; +/** Snapshot row returned by channel status and lifecycle surfaces. */ export type ChannelAccountSnapshot = { accountId: string; name?: string; @@ -220,6 +225,7 @@ export type ChannelGroupContext = { senderE164?: string | null; }; +/** Static capability flags advertised by a channel plugin. */ export type ChannelCapabilities = { chatTypes: Array; polls?: boolean; @@ -384,6 +390,7 @@ export type ChannelThreadingToolContext = { skipCrossContextDecoration?: boolean; }; +/** Channel-owned messaging helpers for target parsing, routing, and payload shaping. */ export type ChannelMessagingAdapter = { normalizeTarget?: (raw: string) => string | undefined; resolveSessionTarget?: (params: { @@ -470,6 +477,7 @@ export type ChannelDirectoryEntry = { export type ChannelMessageActionName = ChannelMessageActionNameFromList; +/** Execution context passed to channel-owned actions on the shared `message` tool. */ export type ChannelMessageActionContext = { channel: ChannelId; action: ChannelMessageActionName; @@ -503,6 +511,7 @@ export type ChannelToolSend = { threadId?: string | null; }; +/** Channel-owned action surface for the shared `message` tool. */ export type ChannelMessageActionAdapter = { /** * Unified discovery surface for the shared `message` tool. @@ -533,6 +542,7 @@ export type ChannelPollResult = { pollId?: string; }; +/** Shared poll input after core has normalized the common poll model. */ export type ChannelPollContext = { cfg: OpenClawConfig; to: string; diff --git a/src/channels/plugins/types.plugin.ts b/src/channels/plugins/types.plugin.ts index b4405a063de..474ee2fdae4 100644 --- a/src/channels/plugins/types.plugin.ts +++ b/src/channels/plugins/types.plugin.ts @@ -44,11 +44,13 @@ export type ChannelConfigUiHint = { itemTemplate?: unknown; }; +/** JSON-schema-like config description published by a channel plugin. */ export type ChannelConfigSchema = { schema: Record; uiHints?: Record; }; +/** Full capability contract for a native channel plugin. */ // oxlint-disable-next-line typescript/no-explicit-any export type ChannelPlugin = { id: ChannelId; diff --git a/src/plugin-sdk/channel-actions.ts b/src/plugin-sdk/channel-actions.ts index 2f6f5748461..03e34b815c6 100644 --- a/src/plugin-sdk/channel-actions.ts +++ b/src/plugin-sdk/channel-actions.ts @@ -7,6 +7,7 @@ import { Type } from "@sinclair/typebox"; import type { TSchema } from "@sinclair/typebox"; import { stringEnum } from "../agents/schema/typebox.js"; +/** Schema helper for channels that expose button rows on the shared `message` tool. */ export function createMessageToolButtonsSchema(): TSchema { return Type.Array( Type.Array( @@ -22,6 +23,7 @@ export function createMessageToolButtonsSchema(): TSchema { ); } +/** Schema helper for channels that accept provider-native card payloads. */ export function createMessageToolCardSchema(): TSchema { return Type.Object( {}, diff --git a/src/plugin-sdk/channel-contract.ts b/src/plugin-sdk/channel-contract.ts index 507166d87f0..c066f7608c9 100644 --- a/src/plugin-sdk/channel-contract.ts +++ b/src/plugin-sdk/channel-contract.ts @@ -1,3 +1,4 @@ +// Pure channel contract types used by plugin implementations and tests. export type { BaseProbeResult, BaseTokenResolution, diff --git a/src/plugin-sdk/channel-feedback.ts b/src/plugin-sdk/channel-feedback.ts index f9f03011ee0..9b230339e51 100644 --- a/src/plugin-sdk/channel-feedback.ts +++ b/src/plugin-sdk/channel-feedback.ts @@ -1,3 +1,4 @@ +// Shared feedback helpers for typing indicators, ack reactions, and status reactions. export { removeAckReactionAfterReply, shouldAckReaction, diff --git a/src/plugin-sdk/channel-inbound.ts b/src/plugin-sdk/channel-inbound.ts index 3f2f2708564..54e2ba1833a 100644 --- a/src/plugin-sdk/channel-inbound.ts +++ b/src/plugin-sdk/channel-inbound.ts @@ -1,3 +1,4 @@ +// Shared inbound parsing helpers for channel plugins. export { createInboundDebouncer, resolveInboundDebounceMs, diff --git a/src/plugin-sdk/channel-pairing.ts b/src/plugin-sdk/channel-pairing.ts index e085dc4e381..cd5e108dee7 100644 --- a/src/plugin-sdk/channel-pairing.ts +++ b/src/plugin-sdk/channel-pairing.ts @@ -10,12 +10,14 @@ import { createScopedPairingAccess } from "./pairing-access.js"; type ScopedPairingAccess = ReturnType; +/** Pairing helpers scoped to one channel account. */ export type ChannelPairingController = ScopedPairingAccess & { issueChallenge: ( params: Omit[0], "channel" | "upsertPairingRequest">, ) => ReturnType; }; +/** Pre-bind the channel id and storage sink for pairing challenges. */ export function createChannelPairingChallengeIssuer(params: { channel: ChannelId; upsertPairingRequest: Parameters[0]["upsertPairingRequest"]; @@ -33,6 +35,7 @@ export function createChannelPairingChallengeIssuer(params: { }); } +/** Build the full scoped pairing controller used by channel runtime code. */ export function createChannelPairingController(params: { core: PluginRuntime; channel: ChannelId; diff --git a/src/plugin-sdk/channel-setup.ts b/src/plugin-sdk/channel-setup.ts index c12027f2944..f83d83c7e91 100644 --- a/src/plugin-sdk/channel-setup.ts +++ b/src/plugin-sdk/channel-setup.ts @@ -17,6 +17,7 @@ export { splitSetupEntries, } from "./setup.js"; +/** Metadata used to advertise an optional channel plugin during setup flows. */ type OptionalChannelSetupParams = { channel: string; label: string; @@ -24,6 +25,7 @@ type OptionalChannelSetupParams = { docsPath?: string; }; +/** Paired setup adapter + setup wizard for channels that may not be installed yet. */ export type OptionalChannelSetupSurface = { setupAdapter: ChannelSetupAdapter; setupWizard: ChannelSetupWizard; @@ -34,6 +36,7 @@ export { createOptionalChannelSetupWizard, } from "./optional-channel-setup.js"; +/** Build both optional setup surfaces from one metadata object. */ export function createOptionalChannelSetupSurface( params: OptionalChannelSetupParams, ): OptionalChannelSetupSurface { diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index c4d324e2e5e..0f24db0f6ce 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -137,6 +137,7 @@ export type ChannelOutboundSessionRouteParams = Parameters< NonNullable >[0]; +/** Remove one of the known provider prefixes from a free-form target string. */ export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string { const trimmed = raw.trim(); for (const provider of providers) { @@ -148,10 +149,15 @@ export function stripChannelTargetPrefix(raw: string, ...providers: string[]): s return trimmed; } +/** Remove generic target-kind prefixes such as `user:` or `group:`. */ export function stripTargetKindPrefix(raw: string): string { return raw.replace(/^(user|channel|group|conversation|room|dm):/i, "").trim(); } +/** + * Build the canonical outbound session route payload returned by channel + * message adapters. + */ export function buildChannelOutboundSessionRoute(params: { cfg: OpenClawConfig; agentId: string; @@ -181,6 +187,7 @@ export function buildChannelOutboundSessionRoute(params: { }; } +/** Options for a channel plugin entry that should register a channel capability. */ type DefineChannelPluginEntryOptions = { id: string; name: string; @@ -227,7 +234,13 @@ type CreatedChannelPluginBase = Pick< > >; -// Shared channel-plugin entry boilerplate for bundled and third-party channels. +/** + * Canonical entry helper for channel plugins. + * + * This wraps `definePluginEntry(...)`, registers the channel capability, and + * optionally exposes extra full-runtime registration such as tools or gateway + * handlers that only make sense outside setup-only registration modes. + */ export function defineChannelPluginEntry({ id, name, @@ -253,7 +266,12 @@ export function defineChannelPluginEntry({ }); } -// Shared setup-entry shape so bundled channels do not duplicate `{ plugin }`. +/** + * Minimal setup-entry helper for channels that ship a separate `setup-entry.ts`. + * + * The setup entry only needs to export `{ plugin }`, but using this helper + * keeps the shape explicit in examples and generated typings. + */ export function defineSetupPluginEntry(plugin: TPlugin) { return { plugin }; } diff --git a/src/plugin-sdk/plugin-entry.ts b/src/plugin-sdk/plugin-entry.ts index e411cb51e89..3493281cf00 100644 --- a/src/plugin-sdk/plugin-entry.ts +++ b/src/plugin-sdk/plugin-entry.ts @@ -52,6 +52,7 @@ export type { OpenClawConfig } from "../config/config.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +/** Options for a plugin entry that registers providers, tools, commands, or services. */ type DefinePluginEntryOptions = { id: string; name: string; @@ -61,6 +62,7 @@ type DefinePluginEntryOptions = { register: (api: OpenClawPluginApi) => void; }; +/** Normalized object shape that OpenClaw loads from a plugin entry module. */ type DefinedPluginEntry = { id: string; name: string; @@ -69,13 +71,20 @@ type DefinedPluginEntry = { register: NonNullable; } & Pick; +/** Resolve either a concrete config schema or a lazy schema factory. */ function resolvePluginConfigSchema( configSchema: DefinePluginEntryOptions["configSchema"] = emptyPluginConfigSchema, ): OpenClawPluginConfigSchema { return typeof configSchema === "function" ? configSchema() : configSchema; } -// Small entry surface for provider and command plugins that do not need channel helpers. +/** + * Canonical entry helper for non-channel plugins. + * + * Use this for provider, tool, command, service, memory, and context-engine + * plugins. Channel plugins should use `defineChannelPluginEntry(...)` from + * `openclaw/plugin-sdk/core` so they inherit the channel capability wiring. + */ export function definePluginEntry({ id, name, diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 0ec4e0ef2f2..514fa1d16a0 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -1,4 +1,5 @@ -// Public auth/onboarding helpers for provider plugins. +// Curated auth + onboarding helpers for provider plugins. +// Keep this surface focused on reusable provider-owned login flows. export type { OpenClawConfig } from "../config/config.js"; export type { SecretInput } from "../config/types.secrets.js"; diff --git a/src/plugin-sdk/provider-onboard.ts b/src/plugin-sdk/provider-onboard.ts index 1537742f453..3c72205f581 100644 --- a/src/plugin-sdk/provider-onboard.ts +++ b/src/plugin-sdk/provider-onboard.ts @@ -1,4 +1,4 @@ -// Public config patch helpers for provider onboarding flows. +// Curated config-patch helpers for provider onboarding flows. export type { OpenClawConfig } from "../config/config.js"; export type { diff --git a/src/plugin-sdk/provider-setup.ts b/src/plugin-sdk/provider-setup.ts index 57f1a94e3bd..a5d48c22c4b 100644 --- a/src/plugin-sdk/provider-setup.ts +++ b/src/plugin-sdk/provider-setup.ts @@ -1,3 +1,4 @@ +// Curated setup helpers for provider plugins that integrate local/self-hosted models. export type { OpenClawPluginApi, ProviderAuthContext, diff --git a/src/plugin-sdk/runtime.ts b/src/plugin-sdk/runtime.ts index ec39c97a549..cec54a53e08 100644 --- a/src/plugin-sdk/runtime.ts +++ b/src/plugin-sdk/runtime.ts @@ -19,6 +19,7 @@ export * from "../logging.js"; export { waitForAbortSignal } from "../infra/abort-signal.js"; export { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js"; +/** Minimal logger contract accepted by runtime-adapter helpers. */ type LoggerLike = { info: (message: string) => void; error: (message: string) => void; diff --git a/src/plugin-sdk/secret-input.ts b/src/plugin-sdk/secret-input.ts index 3d1d9175a0a..1aee32fbf6e 100644 --- a/src/plugin-sdk/secret-input.ts +++ b/src/plugin-sdk/secret-input.ts @@ -14,10 +14,12 @@ export { normalizeSecretInputString, }; +/** Optional version of the shared secret-input schema. */ export function buildOptionalSecretInputSchema() { return buildSecretInputSchema().optional(); } +/** Array version of the shared secret-input schema. */ export function buildSecretInputArraySchema() { return z.array(buildSecretInputSchema()); } diff --git a/src/plugin-sdk/self-hosted-provider-setup.ts b/src/plugin-sdk/self-hosted-provider-setup.ts index 47fe7d6588f..871aa4fb566 100644 --- a/src/plugin-sdk/self-hosted-provider-setup.ts +++ b/src/plugin-sdk/self-hosted-provider-setup.ts @@ -1,3 +1,4 @@ +// Focused self-hosted provider setup helpers for OpenAI-compatible backends. export type { OpenClawPluginApi, ProviderAuthContext, diff --git a/src/plugin-sdk/setup.ts b/src/plugin-sdk/setup.ts index 6865c64e841..f6f1f54f22d 100644 --- a/src/plugin-sdk/setup.ts +++ b/src/plugin-sdk/setup.ts @@ -1,4 +1,4 @@ -// Shared setup wizard/types/helpers for extension setup surfaces and adapters. +// Shared setup wizard/types/helpers for plugin and channel setup surfaces. export type { OpenClawConfig } from "../config/config.js"; export type { DmPolicy, GroupPolicy } from "../config/types.js"; diff --git a/src/plugin-sdk/testing.ts b/src/plugin-sdk/testing.ts index ebb931df1bb..94c8d6d9ff6 100644 --- a/src/plugin-sdk/testing.ts +++ b/src/plugin-sdk/testing.ts @@ -12,6 +12,7 @@ export type { PluginRuntime } from "../plugins/runtime/types.js"; export type { RuntimeEnv } from "../runtime.js"; export type { MockFn } from "../test-utils/vitest-mock-fn.js"; +/** Create a tiny Windows `.cmd` shim fixture for plugin tests that spawn CLIs. */ export async function createWindowsCmdShimFixture(params: { shimPath: string; scriptPath: string; @@ -37,6 +38,7 @@ type ResolveTargetFn = (params: { allowFrom: string[]; }) => ResolveTargetResult; +/** Install a shared test matrix for target-resolution error handling. */ export function installCommonResolveTargetErrorCases(params: { resolveTarget: ResolveTargetFn; implicitAllowFrom: string[]; diff --git a/src/plugin-sdk/webhook-ingress.ts b/src/plugin-sdk/webhook-ingress.ts index 88d71b18248..9b4cacc874c 100644 --- a/src/plugin-sdk/webhook-ingress.ts +++ b/src/plugin-sdk/webhook-ingress.ts @@ -1,3 +1,4 @@ +// Curated webhook helpers for plugin-owned HTTP ingress and webhook targets. export { createBoundedCounter, createFixedWindowRateLimiter, diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index 35d5d52c2a6..a57cbe207b6 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -1,5 +1,6 @@ import type { LogLevel } from "../../logging/levels.js"; +/** Structured logger surface injected into runtime-backed plugin helpers. */ export type RuntimeLogger = { debug?: (message: string, meta?: Record) => void; info: (message: string, meta?: Record) => void; @@ -7,6 +8,7 @@ export type RuntimeLogger = { error: (message: string, meta?: Record) => void; }; +/** Core runtime helpers exposed to trusted native plugins. */ export type PluginRuntimeCore = { version: string; config: { diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index aa1118ecf92..b16ca5e91e6 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -50,6 +50,7 @@ export type SubagentDeleteSessionParams = { deleteTranscript?: boolean; }; +/** Trusted in-process runtime surface injected into native plugins. */ export type PluginRuntime = PluginRuntimeCore & { subagent: { run: (params: SubagentRunParams) => Promise; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 68a4a890132..a49e4e79aa8 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -55,6 +55,7 @@ export type ProviderAuthOptionBag = { [key: string]: unknown; }; +/** Logger passed into plugin registration, services, and CLI surfaces. */ export type PluginLogger = { debug?: (message: string) => void; info: (message: string) => void; @@ -77,6 +78,13 @@ export type PluginConfigValidation = | { ok: true; value?: unknown } | { ok: false; errors: string[] }; +/** + * Config schema contract accepted by plugin manifests and runtime registration. + * + * Plugins can provide a Zod-like parser, a lightweight `validate(...)` + * function, or both. `uiHints` and `jsonSchema` are optional extras for docs, + * forms, and config UIs. + */ export type OpenClawPluginConfigSchema = { safeParse?: (value: unknown) => { success: boolean; @@ -91,6 +99,7 @@ export type OpenClawPluginConfigSchema = { jsonSchema?: Record; }; +/** Trusted execution context passed to plugin-owned agent tool factories. */ export type OpenClawPluginToolContext = { config?: OpenClawConfig; workspaceDir?: string; @@ -127,6 +136,7 @@ export type OpenClawPluginHookOptions = { export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom"; +/** Standard result payload returned by provider auth methods. */ export type ProviderAuthResult = { profiles: Array<{ profileId: string; credential: AuthProfileCredential }>; /** @@ -141,6 +151,7 @@ export type ProviderAuthResult = { notes?: string[]; }; +/** Interactive auth context passed to provider login/setup methods. */ export type ProviderAuthContext = { config: OpenClawConfig; agentDir?: string; @@ -612,12 +623,14 @@ export type ProviderPluginWizardSetup = { }; }; +/** Optional model-picker metadata shown in interactive provider selection flows. */ export type ProviderPluginWizardModelPicker = { label?: string; hint?: string; methodId?: string; }; +/** UI metadata that lets provider plugins appear in onboarding and configure flows. */ export type ProviderPluginWizard = { setup?: ProviderPluginWizardSetup; modelPicker?: ProviderPluginWizardModelPicker; @@ -631,6 +644,7 @@ export type ProviderModelSelectedContext = { workspaceDir?: string; }; +/** Text-inference provider capability registered by a plugin. */ export type ProviderPlugin = { id: string; pluginId?: string; @@ -914,6 +928,7 @@ export type PluginWebSearchProviderEntry = WebSearchProviderPlugin & { pluginId: string; }; +/** Speech capability registered by a plugin. */ export type SpeechProviderPlugin = { id: SpeechProviderId; label: string; @@ -1255,6 +1270,7 @@ export type OpenClawPluginCliContext = { export type OpenClawPluginCliRegistrar = (ctx: OpenClawPluginCliContext) => void | Promise; +/** Context passed to long-lived plugin services. */ export type OpenClawPluginServiceContext = { config: OpenClawConfig; workspaceDir?: string; @@ -1262,6 +1278,7 @@ export type OpenClawPluginServiceContext = { logger: PluginLogger; }; +/** Background service registered by a plugin during `register(api)`. */ export type OpenClawPluginService = { id: string; start: (ctx: OpenClawPluginServiceContext) => void | Promise; @@ -1272,6 +1289,7 @@ export type OpenClawPluginChannelRegistration = { plugin: ChannelPlugin; }; +/** Module-level plugin definition loaded from a native plugin entry file. */ export type OpenClawPluginDefinition = { id?: string; name?: string; @@ -1289,6 +1307,7 @@ export type OpenClawPluginModule = export type PluginRegistrationMode = "full" | "setup-only" | "setup-runtime"; +/** Main registration API injected into native plugin entry files. */ export type OpenClawPluginApi = { id: string; name: string;