Channel approvals
Approval capabilities for channel plugins: approval auth, payload lifecycle and setup guidance, native approval delivery, and the narrower approval runtime subpaths
Most channel plugins do not need approval-specific code. Reach for this page when yours does. Part of the Building channel plugins guide.
Approvals and channel capabilities
Most channel plugins do not need approval-specific code. Core owns same-chat
/approve, shared approval button payloads, and generic fallback delivery.
ChannelPlugin.approvals was removed; put approval delivery/native/render/auth
facts on one approvalCapability object instead. plugin.auth is login/logout
only - core no longer reads approval auth hooks from that object.
Use approvalCapability.delivery only for native approval routing or fallback
suppression, and approvalCapability.render only when a channel truly needs
custom approval payloads instead of the shared renderer.
delivery.shouldBlockForwardingFallback rejects a fallback that cannot enforce
the selected reviewer policy even when no native handler is running. A channel
may require native reviewer delivery and reject all generic forwarding for a
selected policy;
delivery.shouldSuppressForwardingFallback avoids duplicate delivery only while
the native handler is active. Both receive the approval request payload. For a
terminal notice without a cached pending request, core reconstructs it from the
resolved event with createdAtMs and expiresAtMs set to zero; policy checks
should use the nested request payload.
Approval auth
approvalCapability.authorizeActorActionandapprovalCapability.getActionAvailabilityStateare the canonical approval-auth seam.- A channel that enforces host-configured, request-scoped plugin reviewer lists
for native cards, forwarded prompts, and final decisions sets
approvalCapability.supportsScopedPluginApprovalApprovers: true. When a reviewer policy exists for the channel, the host declines plugin approval routes and decisions through an older capability without this marker. Keep the marker absent until every approval path enforces the selected list. - If plugin
/approveauthorization needs a space-qualified reviewer ID, implementapprovalCapability.resolveReviewerSenderId. Core passescfg,accountId,senderId, andspaceId, then uses the result only for plugin approval command authorization. Derive the space from authenticated channel ingress; the callback does not receive the pending request. If it is unknown, returnundefined: core retains the raw sender ID. An unqualified raw ID cannot satisfy a space-qualified policy. The channel'sauthorizeActorActionand Gateway custody still check the exact request when resolving a decision. Older hosts ignore this optional callback and also retain the raw sender ID; channels that supply only unqualified IDs need a host that calls the callback for space-qualified/approvepolicy. - Use
getActionAvailabilityStatefor same-chat approval auth availability. Keep configured approvers available for/approveeven when native delivery is disabled; use native initiating-surface state for delivery/setup guidance instead. - If your channel exposes native exec approvals, use
approvalCapability.getExecInitiatingSurfaceStatefor the initiating-surface/native-client state when it differs from same-chat approval auth. Core uses that exec-specific hook to distinguishenabledvsdisabled, decide whether the initiating channel supports native exec approvals, and include the channel in native-client fallback guidance.createApproverRestrictedNativeApprovalCapability(...)fills this in for the common case. - If a channel can infer stable owner-like DM identities from existing config,
use
createResolvedApproverActionAuthAdapterfromopenclaw/plugin-sdk/approval-runtimeto restrict same-chat/approvewithout adding approval-specific core logic. - If custom approval auth intentionally allows only same-chat fallback, return
markImplicitSameChatApprovalAuthorization({ authorized: true })fromopenclaw/plugin-sdk/approval-auth-runtime; otherwise core treats the result as explicit approver authorization. - If a channel-owned native callback resolves approvals directly, use
isImplicitSameChatApprovalAuthorization(...)before resolving so implicit fallback still goes through the channel's normal actor authorization.
Payload lifecycle and setup guidance
- Use
outbound.shouldSuppressLocalPayloadPromptoroutbound.beforeDeliverPayloadfor channel-specific payload lifecycle behavior such as hiding duplicate local approval prompts or sending typing indicators before delivery. - Use
approvalCapability.describeExecApprovalSetupwhen the channel wants the disabled-path reply to explain the exact config knobs needed to enable native exec approvals. The hook receives{ channel, channelLabel, accountId }; named-account channels should render account-scoped paths such aschannels.<channel>.accounts.<id>.execApprovals.*instead of top-level defaults. - Use
approvalCapability.describePluginApprovalSetupwhen plugin approval failure guidance is safe to show for plugin approval no-route and timeout failures.createApproverRestrictedNativeApprovalCapability(...)does not infer this fromdescribeExecApprovalSetup; pass the same helper explicitly only when plugin and exec approvals truly use the same native setup.
Native approval delivery
If a channel needs native approval delivery, keep channel code focused on
target normalization plus transport/presentation facts. Use
createChannelExecApprovalProfile, createChannelNativeOriginTargetResolver,
createChannelApproverDmTargetResolver, and
createApproverRestrictedNativeApprovalCapability from
openclaw/plugin-sdk/approval-runtime. Put the channel-specific facts behind
approvalCapability.nativeRuntime, ideally via
createChannelApprovalNativeRuntimeAdapter(...) or
createLazyChannelApprovalNativeRuntimeAdapter(...), so core can assemble the
handler and own request filtering, routing, dedupe, expiry, gateway
subscription, and routed-elsewhere notices.
nativeRuntime is split into a few smaller seams:
availability- whether the account is configured and whether a request should be handledpresentation- map the shared approval view model into pending/resolved/expired native payloads or final actionstransport- prepare targets plus send/update/delete native approval messagesinteractions- optional bind/unbind/clear-action hooks for native buttons or reactions, plus an optionalcancelDeliveredhook. ImplementcancelDeliveredwhendeliverPendingregisters in-process or persistent state (such as a reaction target store) so that state can be released if a handler stop cancels the delivery beforebindPendingruns, or whenbindPendingreturns no handleobserve- optional delivery diagnostics hooks
Native approval runtimes can receive three approval kinds: exec, plugin,
and system-agent. A system-agent request asks an operator to approve a
Gateway-side persistent change, such as a config write or Gateway restart.
The runtime must render the typed approval actions and then render the final
application result. An allowed request can finish as applied or not applied;
do not treat the recorded approval alone as proof that the change completed.
Reaction bindings must preserve the explicit approval kind from delivery through decision resolution, including after loading persisted plugin state. iMessage native poll and reaction indexes follow the same rule after a restart.
Other approval helpers:
- Use
settleApprovalReactionfromopenclaw/plugin-sdk/approval-reaction-runtimefor explicitly authorized reaction decisions. It checks the supplied approvers and actor authorization, loads the Gateway resolver lazily, and awaitsclearTargetfor every terminal result (including a losing click) or approval-not-found error. Keep transport identity, route checks, cleanup, and result logging in the plugin. Resolver errors propagate with the binding intact; the channel must hand them to its durable ingress or poller for replay.readApprovalReactionTargetRecordvalidates the shared persisted fields; transport-specific route and author fields still need their own validation. - Await
createApprovalReactionTargetStore().register(...)and.delete(...)before completing registration or cleanup. Both update their memory index before waiting for storage. Optional storage failures retain the existing policy: report the failure, disable persistent access, and keep the memory fallback. Settlement reports resolution only after cleanup completes; cleanup failures propagate without being reported as Gateway resolution failures. - Use
formatChannelApprovalResolvedLabelandbuildSystemAgentApprovalResolvedTextfromopenclaw/plugin-sdk/approval-runtimefor terminal presentation. Rich labels preserve application-status precedence; prose preserves denial precedence, because a denied system change can also reportnot-applied. Both prioritize cancellation. Pass a decision formatter for transport-specific label spelling, and prepare any bounded operation summary before building prose. UseformatApprovalDecisionLabelfor a recorded decision without implying application completion. - Approval account lookup helpers
resolveApprovalRequestAccountIdandresolveApprovalRequestChannelAccountIduseapproval-native-runtime. Their duplicateapproval-runtimeexports and its unusedmatchesApprovalRequestSessionFilterexport have been retired. The core implementations are unchanged. - Use
createNativeApprovalControlRegistryfromopenclaw/plugin-sdk/approval-runtimefor process-local native card tokens. Each instance owns a 1,024-binding FIFO registry and holds its claim through Gateway resolution and the terminal card update. Missing approvals retire their tokens; other failures release the claim for retry. Plugins validate native event scope and authorize the actor before callingsettle, retain their lookup-expiry policy throughreleaseClaimOnLookupExpiry, and useonCompletefor transport-owned cleanup such as manual-prompt suppression. - Use
createNativeApprovalChannelRouteGatesfromopenclaw/plugin-sdk/approval-native-runtimewhen a channel supports both session-origin native delivery and explicit approval forwarding targets. The helper centralizes approval config selection,modehandling, agent/session filters, account binding, session-target matching, and target-list matching while callers still own the channel id, default forwarding mode, account lookup, transport-enabled check, target normalization, and turn-source target resolution. Do not use it to create core-owned channel policy defaults; pass the channel's documented default mode explicitly. The unusedcreateChannelApprovalForwardingEvaluatorexport has been retired; this route-gate helper remains the supported routing path. createNativeApprovalMessagingTargetResolverscentralizes channel matching and{ to, accountId, threadId }normalization for messaging transports whose native approval target is a channel-owned normalized destination. Keep group authorization, approver mapping, and other transport policy in the channel plugin.createChannelNativeOriginTargetResolveruses the shared channel-route matcher by default for{ to, accountId, threadId }targets. PasstargetsMatchonly when a channel has provider-specific equivalence rules, such as Slack timestamp prefix matching. PassnormalizeTargetForMatchwhen the channel needs to canonicalize provider ids before the default route matcher or a customtargetsMatchcallback runs, while preserving the original target for delivery. UsenormalizeTargetonly when the resolved delivery target itself should be canonicalized.- If the channel needs runtime-owned objects such as a client, token, Bolt
app, or webhook receiver, register them through
openclaw/plugin-sdk/channel-runtime-context. The generic runtime-context registry lets core bootstrap capability-driven handlers from channel startup state without adding approval-specific wrapper glue. - Reach for the lower-level
createChannelApprovalHandlerorcreateChannelNativeApprovalRuntimeonly when the capability-driven seam is not expressive enough yet. - Native approval channels must route both
accountIdandapprovalKindthrough those helpers.accountIdkeeps multi-account approval policy scoped to the right bot account, andapprovalKindkeeps exec vs plugin approval behavior available to the channel without hardcoded branches in core. - Core owns approval reroute notices too. Channel plugins should not send
their own "approval went to DMs / another channel" follow-up messages from
createChannelNativeApprovalRuntime; instead, expose accurate origin + approver-DM routing through the shared approval capability helpers and let core aggregate actual deliveries before posting any notice back to the initiating chat. - Preserve the delivered approval id kind end-to-end. Native clients should not guess or rewrite exec vs plugin approval routing from channel-local state.
- Pass that explicit
approvalKindtoresolveApprovalOverGateway. This uses the canonicalapproval.resolveservice and returns the recorded winner when another surface answers first. The older explicitresolveMethodinput remains for command-backed controls; new native actions must not use it or infer kind from an ID. - Different approval kinds can intentionally expose different native surfaces. Current bundled examples: Matrix keeps the same native DM/channel routing and reaction UX for exec and plugin approvals, while still letting auth differ by approval kind; Slack keeps native approval routing available for both exec and plugin ids.
createApproverRestrictedNativeApprovalAdapterstill exists as a compatibility wrapper, but new code should prefer the capability builder and exposeapprovalCapabilityon the plugin.
Narrower approval runtime subpaths
For hot channel entrypoints, prefer these narrower subpaths over the broader
approval-runtime barrel when you only need one part of that family:
openclaw/plugin-sdk/approval-auth-runtimeopenclaw/plugin-sdk/approval-client-runtimeopenclaw/plugin-sdk/approval-delivery-runtimeopenclaw/plugin-sdk/approval-gateway-runtimeopenclaw/plugin-sdk/approval-reference-runtimeopenclaw/plugin-sdk/approval-handler-adapter-runtimeopenclaw/plugin-sdk/approval-handler-runtimeopenclaw/plugin-sdk/approval-native-runtimeopenclaw/plugin-sdk/approval-reply-runtimeopenclaw/plugin-sdk/channel-runtime-context
Likewise, prefer openclaw/plugin-sdk/reply-runtime,
openclaw/plugin-sdk/reply-dispatch-runtime,
openclaw/plugin-sdk/reply-reference, and
openclaw/plugin-sdk/reply-chunking over broader umbrella surfaces when you
do not need them all.