Caricamento in corso...
Caricamento in corso...
Last synced: Today, 22:00
Technical reference for the OpenClaw framework. Real-time synchronization with the official documentation engine.
Use this file to discover all available pages before exploring further.
For the public capability model, plugin shapes, and ownership/execution contracts, see Plugin architecture. This page is the reference for the internal mechanics: load pipeline, registry, runtime hooks, Gateway HTTP routes, import paths, and schema tables.
At startup, OpenClaw does roughly this:
plugins.enabledallowdenyentriesslotsload.pathsregister(api)The safety gates happen before runtime execution. Candidates are blocked when the entry escapes the plugin root, the path is world-writable, or path ownership looks suspicious for non-bundled plugins.
The manifest is the control-plane source of truth. OpenClaw uses it to:
plugins.entries.<id>.configFor native plugins, the runtime module is the data-plane part. It registers actual behavior such as hooks, tools, commands, or provider flows.
Optional manifest
activationsetupregister(...)setupEntryactivation.onStartupactivation.onStartupThe activation planner exposes both an ids-only API for existing callers and a plan API for new diagnostics. Plan entries report why a plugin was selected, separating explicit
activation.*providerschannelscommandAliasessetup.providerscontracts.toolsSetup discovery now prefers descriptor-owned ids such as
setup.providerssetup.cliBackendssetup-apiproviderAuthChoicessetup.requiresRuntime: falserequiresRuntimesetup.providerssetup.cliBackendsOpenClaw does not cache plugin discovery results or direct manifest registry data behind wall-clock windows. Installs, manifest edits, and load-path changes must become visible on the next explicit metadata read or snapshot rebuild. The manifest file parser may keep a bounded file-signature cache keyed by the opened manifest path, inode, size, and timestamps; that cache only avoids re-parsing unchanged bytes and must not cache discovery, registry, owner, or policy answers.
The safe metadata fast path is explicit object ownership, not a hidden cache. Gateway startup hot paths should pass the current
PluginMetadataSnapshotPluginLookUpTableFor plugin loading, the persistent cache layer is runtime loading. It may reuse loader state when code or installed artifacts are actually loaded, such as:
PluginLoaderCacheStateThose caches are data-plane implementation details. They must not answer control-plane questions such as "which plugin owns this provider?" unless the caller deliberately asked for runtime loading.
Do not add persistent or wall-clock caches for:
Callers that rebuild manifest metadata from the persisted installed plugin index reconstruct that registry on demand. The installed index is durable source-plane state; it is not a hidden in-process metadata cache.
Loaded plugins do not directly mutate random core globals. They register into a central plugin registry.
The registry tracks:
Core features then read from that registry instead of talking to plugin modules directly. This keeps loading one-way:
That separation matters for maintainability. It means most core surfaces only need one integration point: "read the registry", not "special-case every plugin module".
Plugins that bind a conversation can react when an approval is resolved.
Use
api.onConversationBindingResolved(...)tsexport default { id: "my-plugin", register(api) { api.onConversationBindingResolved(async (event) => { if (event.status === "approved") { // A binding now exists for this plugin + conversation. console.log(event.binding?.conversationId); return; } // The request was denied; clear any local pending state. console.log(event.request.conversation.conversationId); }); }, };
Callback payload fields:
status"approved""denied"decision"allow-once""allow-always""deny"bindingrequestThis callback is notification-only. It does not change who is allowed to bind a conversation, and it runs after core approval handling finishes.
Provider plugins have three layers:
setup.providers[].envVarsproviderAuthEnvVarsproviderAuthAliasesproviderAuthChoiceschannelEnvVarscatalogdiscoveryapplyConfigDefaultsOpenClaw still owns the generic agent loop, failover, transcript handling, and tool policy. These hooks are the extension surface for provider-specific behavior without needing a whole custom inference transport.
Use manifest
setup.providers[].envVarsproviderAuthEnvVarsproviderAuthAliasesproviderAuthChoicesenvVarsUse manifest
channelEnvVarsFor model/provider plugins, OpenClaw calls hooks in this rough order. The "When to use" column is the quick decision guide. Compatibility-only provider fields that OpenClaw no longer calls, such as
ProviderPlugin.capabilitiessuppressBuiltInModel| # | Hook | What it does | When to use |
|---|---|---|---|
| 1 | text catalog | Publish provider config into text models.providerstext models.json | Provider owns a catalog or base URL defaults |
| 2 | text applyConfigDefaults | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
| -- | (built-in model lookup) | OpenClaw tries the normal registry/catalog path first | (not a plugin hook) |
| 3 | text normalizeModelId | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
| 4 | text normalizeTransport | Normalize provider-family text apitext baseUrl | Provider owns transport cleanup for custom provider ids in the same transport family |
| 5 | text normalizeConfig | Normalize text models.providers.<id> | Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries |
| 6 | text applyNativeStreamingUsageCompat | Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes |
| 7 | text resolveConfigApiKey | Resolve env-marker auth for config providers before runtime auth loading | Provider has provider-owned env-marker API-key resolution; text amazon-bedrock |
| 8 | text resolveSyntheticAuth | Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker |
| 9 | text resolveExternalAuthProfiles | Overlay provider-owned external auth profiles; default text persistencetext runtime-only | Provider reuses external auth credentials without persisting copied refresh tokens; declare text contracts.externalAuthProviders |
| 10 | text shouldDeferSyntheticProfileAuth | Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence |
| 11 | text resolveDynamicModel | Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids |
| 12 | text prepareDynamicModel | Async warm-up, then text resolveDynamicModel | Provider needs network metadata before resolving unknown ids |
| 13 | text normalizeResolvedModel | Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport |
| 14 | text contributeResolvedModelCompat | Contribute compat flags for vendor models behind another compatible transport | Provider recognizes its own models on proxy transports without taking over the provider |
| 15 | text normalizeToolSchemas | Normalize tool schemas before the embedded runner sees them | Provider needs transport-family schema cleanup |
| 16 | text inspectToolSchemas | Surface provider-owned schema diagnostics after normalization | Provider wants keyword warnings without teaching core provider-specific rules |
| 17 | text resolveReasoningOutputMode | Select native vs tagged reasoning-output contract | Provider needs tagged reasoning/final output instead of native fields |
| 18 | text prepareExtraParams | Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup |
| 19 | text createStreamFn | Fully replace the normal stream path with a custom transport | Provider needs a custom wire protocol, not just a wrapper |
| 20 | text wrapStreamFn | Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport |
| 21 | text resolveTransportTurnState | Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity |
| 22 | text resolveWebSocketSessionPolicy | Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy |
| 23 | text formatApiKey | Auth-profile formatter: stored profile becomes the runtime text apiKey | Provider stores extra auth metadata and needs a custom runtime token shape |
| 24 | text refreshOAuth | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared text pi-ai |
| 25 | text buildAuthDoctorHint | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure |
| 26 | text matchesContextOverflowError | Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss |
| 27 | text classifyFailoverReason | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
| 28 | text isCacheTtlEligible | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
| 29 | text buildMissingAuthMessage | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
| 30 | text augmentModelCatalog | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in text models list |
| 31 | text resolveThinkingProfile | Model-specific text /think | Provider exposes a custom thinking ladder or binary label for selected models |
| 32 | text isBinaryThinking | On/off reasoning toggle compatibility hook | Provider exposes only binary thinking on/off |
| 33 | text supportsXHighThinking | text xhigh | Provider wants text xhigh |
| 34 | text resolveDefaultThinkingLevel | Default text /think | Provider owns default text /think |
| 35 | text isModernModelRef | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching |
| 36 | text prepareRuntimeAuth | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential |
| 37 | text resolveUsageAuth | Resolve usage/billing credentials for text /usage | Provider needs custom usage/quota token parsing or a different usage credential |
| 38 | text fetchUsageSnapshot | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser |
| 39 | text createEmbeddingProvider | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin |
| 40 | text buildReplayPolicy | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) |
| 41 | text sanitizeReplayHistory | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers |
| 42 | text validateReplayTurns | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation |
| 43 | text onModelSelected | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active |
normalizeModelIdnormalizeTransportnormalizeConfigIf the provider needs a fully custom wire protocol or custom request executor, that is a different class of extension. These hooks are for provider behavior that still runs on OpenClaw's normal inference loop.
tsapi.registerProvider({ id: "example-proxy", label: "Example Proxy", auth: [], catalog: { order: "simple", run: async (ctx) => { const apiKey = ctx.resolveProviderApiKey("example-proxy").apiKey; if (!apiKey) { return null; } return { provider: { baseUrl: "https://proxy.example.com/v1", apiKey, api: "openai-completions", models: [{ id: "auto", name: "Auto" }], }, }; }, }, resolveDynamicModel: (ctx) => ({ id: ctx.modelId, name: ctx.modelId, provider: "example-proxy", api: "openai-completions", baseUrl: "https://proxy.example.com/v1", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }), prepareRuntimeAuth: async (ctx) => { const exchanged = await exchangeToken(ctx.apiKey); return { apiKey: exchanged.token, baseUrl: exchanged.baseUrl, expiresAt: exchanged.expiresAt, }; }, resolveUsageAuth: async (ctx) => { const auth = await ctx.resolveOAuthToken(); return auth ? { token: auth.token } : null; }, fetchUsageSnapshot: async (ctx) => { return await fetchExampleProxyUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn); }, });
Bundled provider plugins combine the hooks above to fit each vendor's catalog, auth, thinking, replay, and usage needs. The authoritative hook set lives with each plugin under
extensions/Plugins can access selected core helpers via
api.runtimetsconst clip = await api.runtime.tts.textToSpeech({ text: "Hello from OpenClaw", cfg: api.config, }); const result = await api.runtime.tts.textToSpeechTelephony({ text: "Hello from OpenClaw", cfg: api.config, }); const voices = await api.runtime.tts.listVoices({ provider: "elevenlabs", cfg: api.config, });
Notes:
textToSpeechmessages.ttslistVoicesPlugins can also register speech providers via
api.registerSpeechProvider(...)tsapi.registerSpeechProvider({ id: "acme-speech", label: "Acme Speech", isConfigured: ({ config }) => Boolean(config.messages?.tts), synthesize: async (req) => { return { audioBuffer: Buffer.from([]), outputFormat: "mp3", fileExtension: ".mp3", voiceCompatible: false, }; }, });
Notes:
edgemicrosoftFor image/audio/video understanding, plugins register one typed media-understanding provider instead of a generic key/value bag:
tsapi.registerMediaUnderstandingProvider({ id: "google", capabilities: ["image", "audio", "video"], describeImage: async (req) => ({ text: "..." }), transcribeAudio: async (req) => ({ text: "..." }), describeVideo: async (req) => ({ text: "..." }), });
Notes:
api.registerVideoGenerationProvider(...)api.runtime.videoGeneration.*For media-understanding runtime helpers, plugins can call:
tsconst image = await api.runtime.mediaUnderstanding.describeImageFile({ filePath: "/tmp/inbound-photo.jpg", cfg: api.config, agentDir: "/tmp/agent", }); const video = await api.runtime.mediaUnderstanding.describeVideoFile({ filePath: "/tmp/inbound-video.mp4", cfg: api.config, });
For audio transcription, plugins can use either the media-understanding runtime or the older STT alias:
tsconst { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({ filePath: "/tmp/inbound-audio.ogg", cfg: api.config, // Optional when MIME cannot be inferred reliably: mime: "audio/ogg", });
Notes:
api.runtime.mediaUnderstanding.*tools.media.audio{ text: undefined }api.runtime.stt.transcribeAudioFile(...)Plugins can also launch background subagent runs through
api.runtime.subagenttsconst result = await api.runtime.subagent.run({ sessionKey: "agent:main:subagent:search-helper", message: "Expand this query into focused follow-up searches.", provider: "openai", model: "gpt-4.1-mini", deliver: false, });
Notes:
providermodelplugins.entries.<id>.subagent.allowModelOverride: trueplugins.entries.<id>.subagent.allowedModelsprovider/model"*"api.runtime.subagent.deleteSession(...)For web search, plugins can consume the shared runtime helper instead of reaching into the agent tool wiring:
tsconst providers = api.runtime.webSearch.listProviders({ config: api.config, }); const result = await api.runtime.webSearch.search({ config: api.config, args: { query: "OpenClaw plugin runtime helpers", count: 5, }, });
Plugins can also register web-search providers via
api.registerWebSearchProvider(...)Notes:
api.runtime.webSearch.*api.runtime.imageGenerationtsconst result = await api.runtime.imageGeneration.generate({ config: api.config, args: { prompt: "A friendly lobster mascot", size: "1024x1024" }, }); const providers = api.runtime.imageGeneration.listProviders({ config: api.config, });
generate(...)listProviders(...)Plugins can expose HTTP endpoints with
api.registerHttpRoute(...)tsapi.registerHttpRoute({ path: "/acme/webhook", auth: "plugin", match: "exact", handler: async (_req, res) => { res.statusCode = 200; res.end("ok"); return true; }, });
Route fields:
pathauth"gateway""plugin"match"exact""prefix"replaceExistinghandlertrueNotes:
api.registerHttpHandler(...)api.registerHttpRoute(...)authpath + matchreplaceExisting: trueauthexactprefixauth: "plugin"auth: "gateway"gateway.auth.mode = "token""password"operator.writex-openclaw-scopestrusted-proxygateway.auth.mode = "none"x-openclaw-scopesx-openclaw-scopesoperator.writex-openclaw-scopesUse narrow SDK subpaths instead of the monolithic
openclaw/plugin-sdk| Subpath | Purpose |
|---|---|
text openclaw/plugin-sdk/plugin-entry | Plugin registration primitives |
text openclaw/plugin-sdk/channel-core | Channel entry/build helpers |
text openclaw/plugin-sdk/core | Generic shared helpers and umbrella contract |
text openclaw/plugin-sdk/config-schema | Root text openclaw.jsontext OpenClawSchema |
Channel plugins pick from a family of narrow seams —
channel-setupsetup-runtimesetup-adapter-runtimesetup-toolschannel-pairingchannel-contractchannel-feedbackchannel-inboundchannel-lifecyclechannel-reply-pipelinecommand-authsecret-inputwebhook-ingresschannel-targetschannel-actionsapprovalCapabilityRuntime and config helpers live under matching focused
*-runtimeapproval-runtimeagent-runtimelazy-runtimedirectory-runtimetext-runtimeruntime-storesystem-event-runtimeheartbeat-runtimechannel-activity-runtimeconfig-typesplugin-config-runtimeruntime-config-snapshotconfig-mutationconfig-runtimeRepo-internal entry points (per bundled plugin package root):
index.jsapi.jsruntime-api.jssetup-entry.jsExternal plugins should only import
openclaw/plugin-sdk/*src/*Capability-specific subpaths such as
image-generationmedia-understandingspeechPlugins should own channel-specific
describeMessageTool(...)MessagePresentationSend-capable plugins declare what they can render through message capabilities:
presentationtextcontextdividerbuttonsselectdelivery-pinCore decides whether to render the presentation natively or degrade it to text. Do not expose provider-native UI escape hatches from the generic message tool. Deprecated SDK helpers for legacy native schemas remain exported for existing third-party plugins, but new plugins should not use them.
Channel plugins should own channel-specific target semantics. Keep the shared outbound host generic and use the messaging adapter surface for provider rules:
messaging.inferTargetChatType({ to })directgroupchannelmessaging.targetResolver.looksLikeId(raw, normalized)messaging.targetResolver.resolveTarget(...)messaging.resolveOutboundSessionRoute(...)Recommended split:
inferTargetChatTypelooksLikeIdresolveTargettargetPlugins that derive directory entries from config should keep that logic in the plugin and reuse the shared helpers from
openclaw/plugin-sdk/directory-runtimeUse this when a channel needs config-backed peers/groups such as:
The shared helpers in
directory-runtimeChannelDirectoryEntry[]Channel-specific account inspection and id normalization should stay in the plugin implementation.
Provider plugins can define model catalogs for inference with
registerProvider({ catalog: { run(...) { ... } } })catalog.run(...)models.providers{ provider }{ providers }Use
catalogcatalog.ordersimpleprofilepairedlateLater providers win on key collision, so plugins can intentionally override a built-in provider entry with the same provider id.
Compatibility:
discoverycatalogdiscoverycatalogIf your plugin registers a channel, prefer implementing
plugin.config.inspectAccount(cfg, accountId)resolveAccount(...)Why:
resolveAccount(...)openclaw statusopenclaw status --allopenclaw channels statusopenclaw channels resolveRecommended
inspectAccount(...)enabledconfiguredtokenSourcetokenStatusbotTokenSourcebotTokenStatusappTokenSourceappTokenStatussigningSecretSourcesigningSecretStatustokenStatus: "available"configured_unavailableThis lets read-only commands report "configured but unavailable in this command path" instead of crashing or misreporting the account as not configured.
A plugin directory may include a
package.jsonopenclaw.extensionsjson{ "name": "my-pack", "openclaw": { "extensions": ["./src/safety.ts", "./src/tools.ts"], "setupEntry": "./src/setup-entry.ts" } }
Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id becomes
name/<fileBase>If your plugin imports npm deps, install them in that directory so
node_modulesnpm installpnpm installSecurity guardrail: every
openclaw.extensionsSecurity note:
openclaw plugins installnpm install --omit=dev --ignore-scriptspostinstallOptional:
openclaw.setupEntrysetupEntryOptional:
openclaw.startup.deferConfiguredChannelFullLoadUntilAfterListensetupEntryUse this only when
setupEntryIf your full entry still owns any required startup capability, do not enable this flag. Keep the plugin on the default behavior and let OpenClaw load the full entry during startup.
Bundled channels can also publish setup-only contract-surface helpers that core can consult before the full channel runtime is loaded. The current setup promotion surface is:
singleAccountKeysToMovenamedAccountPromotionKeysresolveSingleAccountPromotionTarget(...)Core uses that surface when it needs to promote a legacy single-account channel config into
channels.<id>.accounts.*accounts.defaultThose setup patch adapters keep bundled contract-surface discovery lazy. Import time stays light; the promotion surface is loaded only on first use instead of re-entering bundled channel startup on module import.
When those startup surfaces include gateway RPC methods, keep them on a plugin-specific prefix. Core admin namespaces (
config.*exec.approvals.*wizard.*update.*operator.adminExample:
json{ "name": "@scope/my-channel", "openclaw": { "extensions": ["./index.ts"], "setupEntry": "./setup-entry.ts", "startup": { "deferConfiguredChannelFullLoadUntilAfterListen": true } } }
Channel plugins can advertise setup/discovery metadata via
openclaw.channelopenclaw.installExample:
json{ "name": "@openclaw/nextcloud-talk", "openclaw": { "extensions": ["./index.ts"], "channel": { "id": "nextcloud-talk", "label": "Nextcloud Talk", "selectionLabel": "Nextcloud Talk (self-hosted)", "docsPath": "/channels/nextcloud-talk", "docsLabel": "nextcloud-talk", "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.", "order": 65, "aliases": ["nc-talk", "nc"] }, "install": { "npmSpec": "@openclaw/nextcloud-talk", "localPath": "<bundled-plugin-local-path>", "defaultChoice": "npm" } } }
Useful
openclaw.channeldetailLabeldocsLabelpreferOverselectionDocsPrefixselectionDocsOmitLabelselectionExtrasmarkdownCapableexposure.configuredfalseexposure.setupfalseexposure.docsshowConfiguredshowInSetupexposurequickstartAllowFromallowFromforceAccountBindingpreferSessionLookupForAnnounceTargetOpenClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.jsonOr point
OPENCLAW_PLUGIN_CATALOG_PATHSOPENCLAW_MPM_CATALOG_PATHSPATH{ "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }"packages""plugins""entries"Generated channel catalog entries and provider install catalog entries expose normalized install-source facts next to the raw
openclaw.installdefaultChoiceinstallSourceOfficial external npm entries should prefer an exact
npmSpecexpectedIntegritysource: "path"sourcePathplugins.load.pathsplugins/installs.jsoninstallRecordspluginsContext engine plugins own session context orchestration for ingest, assembly, and compaction. Register them from your plugin with
api.registerContextEngine(id, factory)plugins.slots.contextEngineUse this when your plugin needs to replace or extend the default context pipeline rather than just add memory search or hooks.
tsimport { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core"; export default function (api) { api.registerContextEngine("lossless-claw", (ctx) => ({ info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true }, async ingest() { return { ingested: true }; }, async assemble({ messages, availableTools, citationsMode }) { return { messages, estimatedTokens: 0, systemPromptAddition: buildMemorySystemPromptAddition({ availableTools: availableTools ?? new Set(), citationsMode, }), }; }, async compact() { return { ok: true, compacted: false }; }, })); }
The factory
ctxconfigagentDirworkspaceDirIf your engine does not own the compaction algorithm, keep
compact()tsimport { buildMemorySystemPromptAddition, delegateCompactionToRuntime, } from "openclaw/plugin-sdk/core"; export default function (api) { api.registerContextEngine("my-memory-engine", (ctx) => ({ info: { id: "my-memory-engine", name: "My Memory Engine", ownsCompaction: false, }, async ingest() { return { ingested: true }; }, async assemble({ messages, availableTools, citationsMode }) { return { messages, estimatedTokens: 0, systemPromptAddition: buildMemorySystemPromptAddition({ availableTools: availableTools ?? new Set(), citationsMode, }), }; }, async compact(params) { return await delegateCompactionToRuntime(params); }, })); }
When a plugin needs behavior that does not fit the current API, do not bypass the plugin system with a private reach-in. Add the missing capability.
Recommended sequence:
OpenClawPluginApiapi.runtimeThis is how OpenClaw stays opinionated without becoming hardcoded to one provider's worldview. See the Capability Cookbook for a concrete file checklist and worked example.
When you add a new capability, the implementation should usually touch these surfaces together:
src/<capability>/types.tssrc/<capability>/runtime.tssrc/plugins/types.tssrc/plugins/registry.tssrc/plugins/runtime/*src/test-utils/plugin-registration.tssrc/plugins/contracts/registry.tsdocs/If one of those surfaces is missing, that is usually a sign the capability is not fully integrated yet.
Minimal pattern:
ts// core contract export type VideoGenerationProviderPlugin = { id: string; label: string; generateVideo: (req: VideoGenerationRequest) => Promise<VideoGenerationResult>; }; // plugin API api.registerVideoGenerationProvider({ id: "openai", label: "OpenAI", async generateVideo(req) { return await generateOpenAiVideo(req); }, }); // shared runtime helper for feature/channel plugins const clip = await api.runtime.videoGeneration.generate({ prompt: "Show the robot walking through the lab.", cfg, });
Contract test pattern:
tsexpect(findVideoGenerationProviderIdsForPlugin("openai")).toEqual(["openai"]);
That keeps the rule simple:
© 2024 TaskFlow Mirror
Powered by TaskFlow Sync Engine