Plugin SDK reference
Plugin runtime config and utilities
How plugin code reads the runtime config snapshot, persists config writes, and reuses the shared runtime utilities. Part of the Plugin runtime helpers reference; the api.runtime.config namespace holds the matching namespace entry.
Config loading and writes
Prefer config that was already passed into the active call path, for example api.config during registration or a cfg argument on channel/provider callbacks. This keeps one process snapshot flowing through the work instead of reparsing config on hot paths.
Use api.runtime.config.current() only when a long-lived handler needs the current process snapshot and no config was passed to that function. The returned value is readonly; clone or use a mutation helper before editing.
Tool factories receive ctx.runtimeConfig plus ctx.getRuntimeConfig(). Use the getter inside a long-lived tool's execute callback when config can change after the tool definition was created.
Persist changes with api.runtime.config.mutateConfigFile(...) or api.runtime.config.replaceConfigFile(...). Each write must choose an explicit afterWrite policy:
afterWrite: { mode: "auto" }lets the gateway reload planner decide.afterWrite: { mode: "restart", reason: "..." }forces a clean restart when the writer knows hot reload is unsafe.afterWrite: { mode: "none", reason: "..." }suppresses automatic reload/restart only when the caller owns the follow-up.
The mutation helpers return afterWrite plus a typed followUp summary so callers can log or test whether they requested a restart. The gateway still owns when that restart actually happens.
Use current(), a passed-in cfg, mutateConfigFile(...), or
replaceConfigFile(...) for runtime config access and writes.
For direct SDK imports, prefer the focused config subpaths over the broad openclaw/plugin-sdk/config-runtime compatibility barrel: config-contracts for types, runtime-config-snapshot for current process snapshots, and config-mutation for writes. Read entry-scoped values from api.pluginConfig; use a supplied tool context only for its runtime-wide config snapshot, and keep plugin-specific merging at that boundary. Bundled plugin tests should mock these focused subpaths directly instead of mocking the broad compatibility barrel.
Internal OpenClaw runtime code follows the same direction: load config once at the CLI, gateway, or process boundary, then pass that value through. Successful mutation writes refresh the process runtime snapshot and advance its internal revision; long-lived caches should key off the runtime-owned cache key instead of serializing config locally. Long-lived runtime modules have a zero-tolerance scanner for ambient loadConfig() calls; use a passed cfg, a request context.getRuntimeConfig(), or getRuntimeConfig() at an explicit process boundary.
Provider and channel execution paths must use the active runtime config snapshot, not a file snapshot returned for config readback or editing. File snapshots preserve source values such as SecretRef markers for UI and writes; provider callbacks need the resolved runtime view. When a helper may be called with either the active source snapshot or the active runtime snapshot, route through selectApplicableRuntimeConfig() before reading credentials.
Retained channel monitors can bind createRuntimeConfigReader(cfg) from
openclaw/plugin-sdk/runtime-config-snapshot once at startup. The reader follows
runtime updates when the supplied config belongs to the active runtime, and
preserves an explicitly scoped config otherwise, including when no runtime has
been published yet. Read once per turn and carry that snapshot through admission
and replies. Process-wide controls such as diagnostics should read at the point
of emission.
createChannelInboundDebouncer keeps its returned numeric debounceMs and default
queue timing as startup snapshots. For live timing, pass its existing
resolveDebounceMs(entry) callback and resolve with the bound config reader.
If pending-key or shutdown bookkeeping also depends on the delay, capture one
value on the entry and use it for both bookkeeping and the callback.
A channel's reload.noopPrefixes opts only that channel out of shared-policy
refresh. Declare a prefix only after every retained consumer reads it live or
does not consume it. Undeclared channels still refresh; one channel's declaration
cannot suppress a sibling's reload. A narrower reload.configPrefixes entry can
retain restart behavior under a broader no-op prefix.
Reusable runtime utilities
Import execPolicy from openclaw/plugin-sdk/agent-harness-runtime for the
host's exec mode algebra. execPolicy.resolveExecModePolicy({ mode, security, ask })
returns the mode, security, ask, and auto-review settings. An explicit mode
determines those settings; without one, the helper preserves the security/ask
pair and derives its display mode. execPolicy.minSecurity(a, b) chooses the
more restrictive security value, and execPolicy.maxAsk(a, b) chooses the
stronger approval requirement. Provider adapters retain their own strict input
validation and native sandbox/approval projection.
These typed object members replace the retired minSecurity and maxAsk
exports from infra-runtime. The retired resolveExecModeFromPolicy,
resolveExecPolicyForMode, and resolveExecModePolicy exports can also migrate
to execPolicy.resolveExecModePolicy, selecting the returned fields they need.
Native command probes should use runCommandWithTimeout from
openclaw/plugin-sdk/process-runtime with timeoutMs, the caller's signal, and
killProcessTree: true. Await its result so timeout or cancellation cleanup finishes
before returning. For commands whose output is always UTF-8, such as JSON status
probes, use runUtf8CommandWithTimeout from the same subpath.
Use splitCommandArgs(raw) from the same subpath to group quoted process
arguments. Backslashes and # stay literal; there is no shell expansion.
Unfinished quotes return null unless the caller passes
{ allowUnclosedQuotes: true } to preserve an existing permissive input contract.
Empty quoted arguments are omitted.
Existing process owners can use signalProcessTree. Its onComplete callback runs after Unix
signaling or the bounded Windows taskkill attempt, not proof that every process
exited. Keep the probe pending through cleanup, use detached: true only for a
process group you created, and start Windows tree termination while its root is
still alive.
Channel plugins that deliver agent replies directly can call
renderPresentationForDelivery(handler, payload) from
openclaw/plugin-sdk/interactive-runtime at delivery, after modifying hooks. Supply
the channel's presentationCapabilities and renderPresentation callback; the
callback receives a payload with a normalized, adapted presentation and the
normalized original presentation as its second argument. Use the original for
whole-card text fallbacks that must retain labels clipped by native limits. This
shares core outbound rendering's fallback-text policy and removes the portable
presentation fields after rendering. The callback may be synchronous or async.
Use attachErrorDiagnostic(error, text) from openclaw/plugin-sdk/error-runtime
to attach supplemental operator diagnostics to a thrown error without changing
its identity, message, or failure classification. Mask opaque credentials first;
the helper also redacts recognized secrets and retains at most 2,048 characters.
formatErrorMessageForDisplay(error) includes the nearest attached diagnostic
through nested causes and aggregates. Use it only at terminal display boundaries,
never for retry or authentication decisions. Agent lifecycle errors and terminal
CLI logs render these diagnostics automatically; successful runs remain quiet.
Native RPC error messages retain their original text; agent.wait renders the
supplemental diagnostic at its terminal result boundary.
Channel plugins must admit authenticated agent turns through their injected
api.runtime.agent.runCommandFromIngress(options, runtime) capability. The host
accepts owner authority only from the exact active, trusted plugin registered for
options.messageChannel; guest turns retain their non-owner identity. The public
agentCommandFromIngress SDK helper never accepts a caller-supplied owner claim.
Model-picker integrations use two focused runtime subpaths. Import the typed
ModelPickerAction and ModelPickerCapabilityProfile contracts from
openclaw/plugin-sdk/interactive-runtime. Import
applySessionModelSelection(...) and its result types from
openclaw/plugin-sdk/model-session-runtime; this is the live-session mutation
seam, including its authoritative conflict check and post-commit effects. The
lower-level applyModelOverrideToSessionEntry(...) helper is not a picker
persistence API.
Use applyModelOverrideWithAuthProfileCompatibility(...) only as the direct
persistence fallback when a channel callback cannot enter the full live-session
transaction and already owns an atomic canonical session-entry patch. Pass the
active config, resolved agent directory, entry, effective provider before the
change, and validated selection. The helper mutates that entry only: it keeps a
pinned auth profile when its recorded credential provider or configured alias is
compatible, clears an incompatible pin, and enforces the model-selection lock.
The caller still owns model allowlist validation, atomic persistence,
markLiveSwitchPending, and any post-commit effects. Prefer
applySessionModelSelection(...) whenever the full transaction is available.
Model-picker actions carry only bounded snapshot and catalog tokens. Channel
actor identity, source-message binding, and serialized callback data stay in
the channel's private authenticated envelope. Channel codecs opt into resolving
these actions with { modelPicker: true }; channels without a picker
capability continue to fail closed instead of treating the action as an opaque
callback.
Use inbound botLoopProtection facts for bot-authored inbound messages. Core applies the shared in-memory sliding-window guard before session record and dispatch, without tying the policy to one channel. The guard tracks (scopeId, conversationId, participant pair) keys, counts both directions of a pair together, applies a cooldown once the window budget is exceeded, and prunes inactive entries opportunistically. Retryable transports should also supply a stable eventId; replaying an accepted event while it remains in the active window does not consume another budget slot. Suppressed events add no retained event-identity state.
Channel plugins that expose this behavior to operators should prefer the shared channels.defaults.botLoopProtection shape for baseline budgets, then layer channel/provider-specific overrides on top. The shared config uses seconds because it is user-facing:
type ChannelBotLoopProtectionConfig = { enabled?: boolean; maxEventsPerWindow?: number; windowSeconds?: number; cooldownSeconds?: number;};Pass normalized bot-pair facts with the resolved turn. Core resolves defaults, unit conversion, and enabled semantics:
return { channel: "example", routeSessionKey, storePath, ctxPayload, recordInboundSession, runDispatch, botLoopProtection: { scopeId: "account-1", conversationId: "channel-1", senderId: "bot-a", receiverId: "bot-b", eventId: providerEvent.id, config: channelConfig.botLoopProtection, defaultsConfig: runtimeConfig.channels?.defaults?.botLoopProtection, defaultEnabled: allowBotsMode !== "off", },};Use openclaw/plugin-sdk/pair-loop-guard-runtime directly only for custom
two-party event loops that do not go through the shared inbound reply runner.
Stage timing diagnostics
openclaw/plugin-sdk/time-runtime exports createStageTimingTracker(now?) and
formatStageTimings(stages). The tracker records rounded, nonnegative
durationMs and elapsedMs values. mark(name) measures since the previous
mark; measure(name, run) and measureSync(name, run) record explicit spans,
including failed work, and preserve the callback's result or error. Measured
spans do not advance the checkpoint used by mark.
snapshot() returns { totalMs, stages } with a copied stage array. The optional
clock defaults to Date.now. Formatting produces comma-separated
name:durationMs@elapsedMs entries (with ms units) or none. Callers retain
ownership of log labels, warning thresholds, and when to emit a summary.