openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
262 lines (241 loc) • 15.6 kB
Markdown
---
summary: "Approval capabilities for channel plugins: approval auth, payload lifecycle and setup guidance, native approval delivery, and the narrower approval runtime subpaths"
read_when:
- Your channel exposes native exec or plugin approvals
- You need approval auth, setup guidance, or reaction-based decisions
- You want the narrow approval runtime subpaths instead of the barrel
title: "Channel approvals"
sidebarTitle: "Approvals"
---
Most channel plugins do not need approval-specific code. Reach for this page
when yours does. Part of the [Building channel
plugins](/plugins/sdk-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.authorizeActorAction` and
`approvalCapability.getActionAvailabilityState` are 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 `/approve` authorization needs a space-qualified reviewer ID,
implement `approvalCapability.resolveReviewerSenderId`. Core passes `cfg`,
`accountId`, `senderId`, and `spaceId`, 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,
return `undefined`: core retains the raw sender ID. An unqualified raw ID
cannot satisfy a space-qualified policy. The channel's
`authorizeActorAction` and 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 `/approve` policy.
- Use `getActionAvailabilityState` for same-chat approval auth availability.
Keep configured approvers available for `/approve` even when native delivery
is disabled; use native initiating-surface state for delivery/setup guidance
instead.
- If your channel exposes native exec approvals, use
`approvalCapability.getExecInitiatingSurfaceState` for the
initiating-surface/native-client state when it differs from same-chat
approval auth. Core uses that exec-specific hook to distinguish `enabled` vs
`disabled`, 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 `createResolvedApproverActionAuthAdapter` from
`openclaw/plugin-sdk/approval-runtime` to restrict same-chat `/approve`
without adding approval-specific core logic.
- If custom approval auth intentionally allows only same-chat fallback, return
`markImplicitSameChatApprovalAuthorization({ authorized: true })` from
`openclaw/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.shouldSuppressLocalPayloadPrompt` or
`outbound.beforeDeliverPayload` for channel-specific payload lifecycle
behavior such as hiding duplicate local approval prompts or sending typing
indicators before delivery.
- Use `approvalCapability.describeExecApprovalSetup` when 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 as
`channels.<channel>.accounts.<id>.execApprovals.*` instead of top-level
defaults.
- Use `approvalCapability.describePluginApprovalSetup` when plugin approval
failure guidance is safe to show for plugin approval no-route and timeout
failures. `createApproverRestrictedNativeApprovalCapability(...)` does not
infer this from `describeExecApprovalSetup`; 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 handled
- `presentation` - map the shared approval view model into
pending/resolved/expired native payloads or final actions
- `transport` - prepare targets plus send/update/delete native approval
messages
- `interactions` - optional bind/unbind/clear-action hooks for native buttons
or reactions, plus an optional `cancelDelivered` hook. Implement
`cancelDelivered` when `deliverPending` registers in-process or persistent
state (such as a reaction target store) so that state can be released if a
handler stop cancels the delivery before `bindPending` runs, or when
`bindPending` returns no handle
- `observe` - 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 `settleApprovalReaction` from
`openclaw/plugin-sdk/approval-reaction-runtime` for explicitly authorized
reaction decisions. It checks the supplied approvers and actor authorization,
loads the Gateway resolver lazily, and awaits `clearTarget` for 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. `readApprovalReactionTargetRecord` validates 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 `formatChannelApprovalResolvedLabel` and
`buildSystemAgentApprovalResolvedText` from
`openclaw/plugin-sdk/approval-runtime` for terminal presentation.
Rich labels preserve application-status precedence; prose preserves denial
precedence, because a denied system change can also report `not-applied`.
Both prioritize cancellation. Pass a decision formatter for transport-specific
label spelling, and prepare any bounded operation summary before building prose.
Use `formatApprovalDecisionLabel` for a recorded decision without implying
application completion.
- Approval account lookup helpers `resolveApprovalRequestAccountId` and
`resolveApprovalRequestChannelAccountId` use `approval-native-runtime`. Their
duplicate `approval-runtime` exports and its unused
`matchesApprovalRequestSessionFilter` export have been retired. The core
implementations are unchanged.
- Use `createNativeApprovalControlRegistry` from
`openclaw/plugin-sdk/approval-runtime` for 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 calling `settle`,
retain their lookup-expiry policy through `releaseClaimOnLookupExpiry`, and
use `onComplete` for transport-owned cleanup such as manual-prompt suppression.
- Use `createNativeApprovalChannelRouteGates` from
`openclaw/plugin-sdk/approval-native-runtime` when a channel supports both
session-origin native delivery and explicit approval forwarding targets. The
helper centralizes approval config selection, `mode` handling, 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 unused `createChannelApprovalForwardingEvaluator` export has been retired;
this route-gate helper remains the supported routing path.
- `createNativeApprovalMessagingTargetResolvers` centralizes 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.
- `createChannelNativeOriginTargetResolver` uses the shared channel-route
matcher by default for `{ to, accountId, threadId }` targets. Pass
`targetsMatch` only when a channel has provider-specific equivalence rules,
such as Slack timestamp prefix matching. Pass `normalizeTargetForMatch` when
the channel needs to canonicalize provider ids before the default route
matcher or a custom `targetsMatch` callback runs, while preserving the
original target for delivery. Use `normalizeTarget` only 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 `createChannelApprovalHandler` or
`createChannelNativeApprovalRuntime` only when the capability-driven seam is
not expressive enough yet.
- Native approval channels must route both `accountId` and `approvalKind`
through those helpers. `accountId` keeps multi-account approval policy
scoped to the right bot account, and `approvalKind` keeps 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 `approvalKind` to `resolveApprovalOverGateway`. This uses
the canonical `approval.resolve` service and returns the recorded winner when
another surface answers first. The older explicit `resolveMethod` input
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.
- `createApproverRestrictedNativeApprovalAdapter` still exists as a
compatibility wrapper, but new code should prefer the capability builder
and expose `approvalCapability` on 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-runtime`
- `openclaw/plugin-sdk/approval-client-runtime`
- `openclaw/plugin-sdk/approval-delivery-runtime`
- `openclaw/plugin-sdk/approval-gateway-runtime`
- `openclaw/plugin-sdk/approval-reference-runtime`
- `openclaw/plugin-sdk/approval-handler-adapter-runtime`
- `openclaw/plugin-sdk/approval-handler-runtime`
- `openclaw/plugin-sdk/approval-native-runtime`
- `openclaw/plugin-sdk/approval-reply-runtime`
- `openclaw/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.