Plugin architecture internals
기준일: 2026-07-26
공식 기준: Plugin architecture internals
Plugin architecture internals 문서는 OpenClaw 공식 문서(plugins/architecture-internals)를 한국어로 정리한 가이드입니다. Plugin architecture internals: load pipeline, registry, runtime hooks, HTTP routes, and reference tables 명령·설정 키·코드 예시는 공식 문서를 그대로 보존하며, 해석과 절차 안내는 한국어로 제공합니다. 최종 동작은 설치된 CLI 버전과 공식 원문을 확인하세요.
핵심 요약
Plugin architecture internals: load pipeline, registry, runtime hooks, HTTP routes, and reference tables
한국어 가이드 범위: plugins/architecture-internals 경로의 설정·명령·제약·예시를 학습용으로 재구성합니다.
문서 구성
공식 문서의 주요 섹션은 다음과 같습니다.
- Load pipeline
- Manifest-first behavior
- Plugin cache boundary
- Registry model
- Conversation binding callbacks
- Provider runtime hooks
- Hook order and usage
- Provider example
- Built-in examples
- Runtime helpers
- api.runtime.imageGeneration
- Gateway HTTP routes
- Plugin SDK import paths
- Message tool schemas
- Channel target resolution
- Config-backed directories
- Provider catalogs
- Read-only channel inspection
- Package packs
- Channel catalog metadata
- Context engine plugins
- Adding a new capability
- Capability checklist
- Capability template
- 관련 문서
상세 내용
본문
For the public capability model, plugin shapes, and ownership/execution contracts, see Plugin architecture. This page covers the internal mechanics: load pipeline, registry, runtime hooks, Gateway HTTP routes, import paths, and schema tables.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Load pipeline
- discover candidate plugin roots 2. read native or compatible bundle manifests and package metadata 3. reject unsafe candidates 4. normalize plugin config (
plugins.enabled,allow,deny,entries,slots,load.paths) 5. decide enablement for each candidate 6. load enabled native modules: built bundled modules use a native loader; third-party local source TypeScript uses the emergency Jiti fallback 7. call nativeregister(api)hooks and collect registrations into the plugin registry 8. expose the registry to commands/runtime surfaces
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- its resolved entry escapes the plugin root
- its path (or its root directory) is world-writable
- for non-bundled plugins, path ownership does not match the current uid (or root)
Manifest-first behavior
The manifest is the control-plane source of truth. OpenClaw uses it to:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- identify the plugin
- discover declared channels/skills/config schema or bundle capabilities
- validate
plugins.entries.<id>.config - augment Control UI labels/placeholders
- show install/catalog metadata
- preserve cheap activation and setup descriptors without loading plugin runtime
- CLI loading narrows to plugins that own the requested primary command
- channel setup/plugin resolution narrows to plugins that own the requested
- explicit provider setup/runtime resolution narrows to plugins that own the
- Gateway startup planning uses
activation.onStartupfor explicit startup
Reason (from activation.* hints) |
Reason (from manifest ownership) |
|---|---|
activation-agent-harness-hint |
— |
activation-capability-hint |
— |
activation-channel-hint |
manifest-channel-owner (channels) |
activation-command-hint |
manifest-command-alias (commandAliases) |
activation-provider-hint |
manifest-provider-owner (providers), manifest-setup-provider-owner (setup.providers) |
activation-route-hint |
— |
| — (hook trigger has no hint variant) | manifest-hook-owner (hooks), manifest-tool-contract (contracts.tools) |
Plugin cache boundary
OpenClaw 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 keeps a bounded file-signature cache keyed by the opened manifest path plus device/inode, size, and mtime/ctime; that cache only avoids re-parsing unchanged bytes and must not cache discovery, registry, owner, or policy answers.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
PluginLoaderCacheStateand compatible active runtime registries- jiti/module caches and public-surface loader caches used to avoid importing
- filesystem caches for installed plugin artifacts
- short-lived per-call maps for path normalization or duplicate resolution
- discovery results
- direct manifest registries
- manifest registries reconstructed from the installed plugin index
- provider owner lookup, model suppression, provider policy, or public-artifact
- any other manifest-derived answer where a changed manifest, installed index,
Registry model
Loaded plugins do not directly mutate random core globals. They register into a central plugin registry (PluginRegistry in src/plugins/registry-types.ts), which tracks plugin records (identity, source, origin, status, diagnostics) plus arrays for every capability: tools, legacy hooks and typed hooks, channels, providers, gateway RPC handlers, HTTP routes, CLI registrars, background services, plugin-owned commands, and dozens more typed provider families (speech, embeddings, image/video/music generation, web fetch/search, agent harnesses, session actions, and so on).
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- plugin module -> registry registration
- core runtime -> registry consumption
Conversation binding callbacks
Plugins that bind a conversation can react when an approval is resolved.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
status:"approved"or"denied"decision:"allow-once","allow-always", or"deny"binding: the resolved binding for approved requestsrequest: the original request summary, detach hint, sender id, and
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);
});
},
};
Provider runtime hooks
setup.providers[].envVars, providerAuthAliases, providerAuthChoices, and channelConfigs.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- Manifest metadata for cheap pre-runtime lookup:
- Config-time hooks:
catalogplusapplyConfigDefaults. - Runtime hooks: 40+ optional hooks covering auth, model resolution,
Hook order and usage
For 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.capabilities and suppressBuiltInModel, are intentionally not listed here.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
| Hook | What it does | When to use |
|---|---|---|
catalog |
Publish provider config into models.providers during models.json generation |
Provider owns a catalog or base URL defaults |
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) |
normalizeModelId |
Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
normalizeTransport |
Normalize provider-family api / baseUrl before generic model assembly |
Provider owns transport cleanup for custom provider ids in the same transport family |
normalizeConfig |
Normalize models.providers.<id> before runtime/provider resolution |
Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries |
applyNativeStreamingUsageCompat |
Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes |
resolveConfigApiKey |
Resolve env-marker auth for config providers before runtime auth loading | Providers expose their own env-marker API-key resolution hooks |
resolveSyntheticAuth |
Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker |
resolveExternalAuthProfiles |
Overlay provider-owned external auth profiles; default persistence is runtime-only for CLI/app-owned creds |
Provider reuses external auth credentials without persisting copied refresh tokens; declare contracts.externalAuthProviders in the manifest |
shouldDeferSyntheticProfileAuth |
Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence |
resolveDynamicModel |
Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids |
prepareDynamicModel |
Async warm-up, then resolveDynamicModel runs again |
Provider needs network metadata before resolving unknown ids |
normalizeResolvedModel |
Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport |
normalizeToolSchemas |
Normalize tool schemas before the embedded runner sees them | Provider needs transport-family schema cleanup |
inspectToolSchemas |
Surface provider-owned schema diagnostics after normalization | Provider wants keyword warnings without teaching core provider-specific rules |
resolveReasoningOutputMode |
Select native vs tagged reasoning-output contract | Provider needs tagged reasoning/final output instead of native fields |
prepareExtraParams |
Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup |
createStreamFn |
Fully replace the normal stream path with a custom transport | Provider needs a custom wire protocol, not just a wrapper |
wrapStreamFn |
Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport |
resolveTransportTurnState |
Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity |
resolveWebSocketSessionPolicy |
Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy |
formatApiKey |
Auth-profile formatter: stored profile becomes the runtime apiKey string |
Provider stores extra auth metadata and needs a custom runtime token shape |
refreshOAuth |
OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared OpenClaw refreshers |
buildAuthDoctorHint |
Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure |
matchesContextOverflowError |
Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss |
classifyFailoverReason |
Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
isCacheTtlEligible |
Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
buildMissingAuthMessage |
Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
augmentModelCatalog |
Synthetic/final catalog rows appended after discovery (deprecated, see below) | Provider needs synthetic forward-compat rows in models list and pickers |
resolveThinkingProfile |
Model-specific /think level set, display labels, and default |
Provider exposes a custom thinking ladder or binary label for selected models |
isBinaryThinking |
On/off reasoning toggle compatibility hook | Provider exposes only binary thinking on/off |
supportsXHighThinking |
xhigh reasoning support compatibility hook |
Provider wants xhigh on only a subset of models |
resolveDefaultThinkingLevel |
Default /think level compatibility hook |
Provider owns default /think policy for a model family |
isModernModelRef |
Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching |
prepareRuntimeAuth |
Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential |
resolveUsageAuth |
Resolve usage/billing credentials for /usage and related status surfaces |
Provider needs custom usage/quota token parsing or a different usage credential |
fetchUsageSnapshot |
Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser |
createEmbeddingProvider |
Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin |
buildReplayPolicy |
Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) |
sanitizeReplayHistory |
Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers |
validateReplayTurns |
Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation |
onModelSelected |
Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active |
Provider example
api.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);
},
});
Built-in examples
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/; this page illustrates the shapes rather than mirroring the list.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Runtime helpers
Plugins can access selected core helpers via api.runtime. For TTS:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
textToSpeechreturns the normal core TTS output payload for file/voice-note surfaces.- Uses core
ttsconfiguration and provider selection. - Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
listVoicesis optional per provider. Use it for vendor-owned voice pickers or setup flows.- Core passes a resolved request deadline to provider
listVoiceshooks; provider-specific timeout settings may override it. - Voice listings can include richer metadata such as locale, gender, and personality tags for provider-aware pickers.
- OpenAI and ElevenLabs support telephony today. Microsoft does not.
- Keep TTS policy, fallback, and reply delivery in core.
- Use speech providers for vendor-owned synthesis behavior.
- Legacy Microsoft
edgeinput is normalized to themicrosoftprovider id. - The preferred ownership model is company-oriented: one vendor plugin can own
- Keep orchestration, fallback, config, and channel wiring in core.
- Keep vendor behavior in the provider plugin.
- Additive expansion should stay typed: new optional methods, new optional
- Video generation already follows the same pattern:
- core owns the capability contract and runtime helper
- vendor plugins register
api.registerVideoGenerationProvider(...) - feature/channel plugins consume
api.runtime.videoGeneration.* api.runtime.mediaUnderstanding.*is the preferred shared surface forextractStructuredWithModel(...)is the plugin-facing seam for bounded- Uses core media-understanding audio configuration (
tools.media.audio) and provider fallback order. - Returns
{ text: undefined }when no transcription output is produced (for example skipped/unsupported input). providerandmodelare optional per-run overrides, not persistent session changes.toolsAlsoAllowaccepts exact, uniquely owned tool names registered by the calling plugin. Core and ambiguous names are rejected. It is additive to the normal profile, but operator allowlists and denies remain authoritative.- OpenClaw only honors those override fields for trusted callers.
const 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,
});
api.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,
};
},
});
api.registerMediaUnderstandingProvider({
id: "google",
capabilities: ["image", "audio", "video"],
describeImage: async (req) => ({ text: "..." }),
transcribeAudio: async (req) => ({ text: "..." }),
describeVideo: async (req) => ({ text: "..." }),
});
const 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,
});
const extraction = await api.runtime.mediaUnderstanding.extractStructuredWithModel({
provider: "codex",
model: "gpt-5.6-sol",
input: [
{
type: "image",
buffer: receiptImageBuffer,
fileName: "receipt.png",
mime: "image/png",
},
{ type: "text", text: "Use the printed fields as the source of truth." },
],
instructions: "Return entities and searchable tags.",
schemaName: "example.evidence",
jsonSchema: {
type: "object",
properties: {
entities: { type: "array", items: { type: "string" } },
tags: { type: "array", items: { type: "string" } },
},
},
cfg: api.config,
});
api.runtime.imageGeneration
주요 항목:
generate(...): generate an image using the configured image-generation provider chain.listProviders(...): list available image-generation providers and their capabilities.
const 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,
});
Gateway HTTP routes
Plugins can expose HTTP endpoints with api.registerHttpRoute(...).
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
path: route path under the gateway HTTP server.auth: required,"gateway"or"plugin". Use"gateway"to require normal gateway auth, or"plugin"for plugin-managed auth/webhook verification.match: optional."exact"(default) or"prefix".handleUpgrade: optional handler for WebSocket upgrade requests on the same route.replaceExisting: optional. Allows the same plugin to replace its own existing route registration.handler: returntruewhen the route handled the request.api.registerHttpHandler(...)was removed and will cause a plugin-load error. Useapi.registerHttpRoute(...)instead.- Plugin routes must declare
authexplicitly. - Exact
path + matchconflicts are rejected unlessreplaceExisting: true, and one plugin cannot replace another plugin's route. - Overlapping routes with different
authlevels are rejected. Keepexact/prefixfallthrough chains on the same auth level only. auth: "plugin"routes do not receive operator runtime scopes automatically. They are for plugin-managed webhooks/signature verification, not privileged Gateway helper calls.auth: "gateway"routes run inside a Gateway request runtime scope. The default surface (gatewayRuntimeScopeSurface: "write-default") is intentionally conservative:- shared-secret bearer auth (
gateway.auth.mode = "token"/"password") and any non-trusted-proxy auth method get a singleoperator.writescope, even if the caller sendsx-openclaw-scopes trusted-proxycallers without an explicitx-openclaw-scopesheader also keep the legacyoperator.write-only surfacetrusted-proxycallers that do sendx-openclaw-scopesget the declared scopes instead- a route can opt into
gatewayRuntimeScopeSurface: "trusted-operator"to always honorx-openclaw-scopesfor identity-bearing auth modes (falling back to the full CLI default scope set when the header is absent) - Sandboxed external Control UI tabs backed by
auth: "gateway"routes use a short-lived signed cookie grant minted only by authenticated bootstrap; plugin-auth tabs keep their direct iframe path. Before mounting, the parent runs a route-owned probe inside the same opaque sandbox and fails closed when browser privacy policy blocks the cookie. The grant is bound to the owning plugin, matched route root, and current auth generation; its process-random cookie name prevents trusted same-host Gateways from overwriting one another, but cookies never isolate TCP ports. The Gateway hostname is therefore one credential boundary: do not cohost mutually untrusted services on that hostname, including other ports. Route dispatch rejects reuse against a nested route owned by another plugin. Because sandbox descendants are cross-site for cookie purposes, the grant accepts onlyGETandHEADwithoperator.read; mutations and WebSocket upgrades stay on explicit Gateway-authenticated surfaces. The cookie intentionally cannot use CHIPS: current browsers include a cross-site-ancestor bit in the partition key, so nested opaque sandbox frames would lose access to same-route assets. The cookie requires a secure context and browser permission for cross-site cookies, so gateway-auth external tabs are unavailable on plain-HTTP LAN origins or under full third-party-cookie blocking; use HTTPS/Tailscale Serve or browser-trusted loopback with a compatible cookie policy. - The grant prevents Gateway bearer-token disclosure and accidental route/scope reuse; it does not create a security boundary between native plugins. Native plugin code and the UI content it serves remain part of the same trusted in-process plugin boundary.
- Practical rule: do not assume a gateway-auth plugin route is an implicit admin surface. If your route needs admin-only behavior, opt into
trusted-operatorscope surface, require an identity-bearing auth mode, and document the explicitx-openclaw-scopesheader contract. - After route matching and authentication, ordinary handlers participate in Gateway root-work admission. A prepared or restarting Gateway returns
503before invoking the handler. The narrow exception is a manifest-entitledauth: "gateway"route that also opts into the route-specifictrusted-operatorsurface; it remains reachable so suspension control dispatch cannot be stranded, while ordinary sibling routes from the same plugin remain behind the admission boundary. WebSockethandleUpgradeownership uses the same atomic admission boundary; once the handler accepts a socket, the socket's later lifetime is plugin-owned and is not tracked by this boundary.
api.registerHttpRoute({
path: "/acme/webhook",
auth: "plugin",
match: "exact",
handler: async (_req, res) => {
res.statusCode = 200;
res.end("ok");
return true;
},
});
Plugin SDK import paths
Use narrow SDK subpaths instead of the monolithic openclaw/plugin-sdk root barrel when authoring new plugins. Core subpaths:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
index.js— bundled plugin entryapi.js— helper/types barrelruntime-api.js— runtime-only barrelsetup-entry.js— setup plugin entry
| Subpath | Purpose |
|---|---|
openclaw/plugin-sdk/plugin-entry |
Plugin registration primitives |
openclaw/plugin-sdk/channel-core |
Channel entry/build helpers |
openclaw/plugin-sdk/core |
Generic shared helpers and umbrella contract |
Message tool schemas
Plugins should own channel-specific describeMessageTool(...) schema contributions for non-message primitives such as reactions, reads, and polls. Shared send presentation should use the generic MessagePresentation contract instead of provider-native button, component, block, or card fields. See Message Presentation for the contract, fallback rules, provider mapping, and plugin author checklist.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
presentationfor semantic presentation blocks (text,context,delivery-pinfor pinned-delivery requests
Channel target resolution
Channel plugins should own channel-specific target semantics. Keep the shared outbound host generic and use the messaging adapter surface for provider rules:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
messaging.inferTargetChatType({ to })decides whether a normalized targetmessaging.targetResolver.looksLikeId(raw, normalized)tells core whether anmessaging.targetResolver.reservedLiteralslists bare words that aremessaging.targetResolver.resolveTarget(...)is the plugin fallback whenmessaging.resolveOutboundSessionRoute(...)owns provider-specific session- Use
inferTargetChatTypefor category decisions that should happen before - Use
looksLikeIdfor "treat this as an explicit/native target id" checks. - Use
resolveTargetfor provider-specific normalization fallback, not for - Keep provider-native ids like chat ids, thread ids, JIDs, handles, and room
Config-backed directories
Plugins that derive directory entries from config should keep that logic in the plugin and reuse the shared helpers from openclaw/plugin-sdk/directory-runtime.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- allowlist-driven DM peers
- configured channel/group maps
- account-scoped static directory fallbacks
- query filtering
- limit application
- deduping/normalization helpers
- building
ChannelDirectoryEntry[]
Provider catalogs
Provider plugins can define model catalogs for inference with registerProvider({ catalog: { run(...) { ... } } }).
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
{ provider }for one provider entry{ providers }for multiple provider entriessimple: plain API-key or env-driven providersprofile: providers that appear when auth profiles existpaired: providers that synthesize multiple related provider entrieslate: last pass, after other implicit providersdiscoverystill works as a legacy alias, but emits a deprecation warning- if both
cataloganddiscoveryare registered, OpenClaw usescatalog augmentModelCatalogis deprecated; bundled providers should publish
Read-only channel inspection
If your plugin registers a channel, prefer implementing plugin.config.inspectAccount(cfg, accountId) alongside resolveAccount(...).
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
resolveAccount(...)is the runtime path. It is allowed to assume credentials- Read-only command paths such as
openclaw status,openclaw status --all, - Return descriptive account state only.
- Preserve
enabledandconfigured. - Include credential source/status fields when relevant, such as:
tokenSource,tokenStatusbotTokenSource,botTokenStatusappTokenSource,appTokenStatussigningSecretSource,signingSecretStatus- You do not need to return raw token values just to report read-only
- Use
configured_unavailablewhen a credential is configured via SecretRef but
Package packs
A plugin directory may include a package.json with openclaw.extensions:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- channel registration itself
- any HTTP routes that must be available before the gateway starts listening
- any gateway methods, tools, or services that must exist during that same window
singleAccountKeysToMovenamedAccountPromotionKeysresolveSingleAccountPromotionTarget(...)
{
"name": "my-pack",
"openclaw": {
"extensions": ["./src/safety.ts", "./src/tools.ts"],
"setupEntry": "./src/setup-entry.ts"
}
}
{
"name": "@scope/my-channel",
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"startup": {
"deferConfiguredChannelFullLoadUntilAfterListen": true
}
}
}
Channel catalog metadata
Channel plugins can advertise setup/discovery metadata via openclaw.channel and install hints via openclaw.install. This keeps the core catalog data-free.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
detailLabel: secondary label for richer catalog/status surfacesdocsLabel: override link text for the docs linkpreferOver: lower-priority plugin/channel ids this catalog entry should outrankselectionDocsPrefix,selectionDocsOmitLabel,selectionExtras: selection-surface copy controlsmarkdownCapable: marks the channel as markdown-capable for outbound formatting decisionsexposure.configured: hide the channel from configured-channel listing surfaces when set tofalseexposure.setup: hide the channel from interactive setup/configure pickers when set tofalseexposure.docs: mark the channel as internal/private for docs navigation surfacesquickstartAllowFrom: opt the channel into the standard quickstartallowFromflowforceAccountBinding: require explicit account binding even when only one account existspreferSessionLookupForAnnounceTarget: prefer session lookup when resolving announce targets~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.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"
}
}
}
Context engine plugins
Context engine plugins own session context orchestration for ingest, assembly, and compaction. Register them from your plugin with api.registerContextEngine(id, factory), then select the active engine with plugins.slots.contextEngine.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
api.registerContextEngine("lossless-claw", (ctx) => ({
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
async ingest() {
return { ingested: true };
},
async assemble({ messages, sessionKey, availableTools, citationsMode }) {
return {
messages,
estimatedTokens: 0,
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
agentSessionKey: sessionKey,
}),
};
},
async compact() {
return { ok: true, compacted: false };
},
}));
}
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
} from "openclaw/plugin-sdk/core";
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, sessionKey, availableTools, citationsMode }) {
return {
messages,
estimatedTokens: 0,
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
agentSessionKey: sessionKey,
}),
};
},
async compact(params) {
return await delegateCompactionToRuntime(params);
},
}));
}
Adding a new capability
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.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Capability checklist
When you add a new capability, the implementation should usually touch these surfaces together:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- core contract types in
src/<capability>/types.ts - core runner/runtime helper in
src/<capability>/runtime.ts - plugin API registration surface in
src/plugins/types.ts - plugin registry wiring in
src/plugins/registry.ts - plugin runtime exposure in
src/plugins/runtime/*when feature/channel - capture/test helpers in
src/test-utils/plugin-registration.ts - ownership/contract assertions in
src/plugins/contracts/registry.ts - operator/plugin docs in
docs/
Capability template
Contract test pattern (src/plugins/contracts/registry.ts exposes ownership lookups such as providerContractPluginIds; tests assert a plugin's contracts.videoGenerationProviders list matches what it actually registers):
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- core owns the capability contract + orchestration
- vendor plugins own vendor implementations
- feature/channel plugins consume runtime helpers
- contract tests keep ownership explicit
// core contract
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,
});
expect(pluginManifest.contracts?.videoGenerationProviders).toEqual(["openai"]);
관련 문서
주요 항목:
- Plugin architecture — public capability model and shapes
- Plugin SDK subpaths
- Plugin SDK setup
- Building plugins
실습 체크리스트
- 공식 문서와 로컬 버전을 대조합니다:
https://docs.openclaw.ai/plugins/architecture-internals - 관련 CLI는
openclaw --help및 하위 명령--help로 옵션을 확인합니다. - 설정 변경 시
openclaw config/openclaw doctor로 유효성을 검사합니다. - Gateway·채널·플러그인 변경 후에는 필요 시 Gateway를 재시작합니다.
자주 쓰는 명령·설정 예시
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);
});
},
};
api.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);
},
});
const 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,
});
api.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,
};
},
});
api.registerMediaUnderstandingProvider({
id: "google",
capabilities: ["image", "audio", "video"],
describeImage: async (req) => ({ text: "..." }),
transcribeAudio: async (req) => ({ text: "..." }),
describeVideo: async (req) => ({ text: "..." }),
});
const 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,
});
const extraction = await api.runtime.mediaUnderstanding.extractStructuredWithModel({
provider: "codex",
model: "gpt-5.6-sol",
input: [
{
type: "image",
buffer: receiptImageBuffer,
fileName: "receipt.png",
mime: "image/png",
},
{ type: "text", text: "Use the printed fields as the source of truth." },
],
instructions: "Return entities and searchable tags.",
schemaName: "example.evidence",
jsonSchema: {
type: "object",
properties: {
entities: { type: "array", items: { type: "string" } },
tags: { type: "array", items: { type: "string" } },
},
},
cfg: api.config,
});
관련 링크
이 가이드는 공식 문서를 한국어 학습용으로 재구성한 것입니다. 옵션 기본값·플래그 이름은 설치 버전에 따라 달라질 수 있습니다.