# Channel approvals

> Approval capabilities for channel plugins: approval auth, payload lifecycle and setup guidance, native approval delivery, and the narrower approval runtime subpaths

- 网址：https://funcoding.ai/agents/openclaw/plugins/sdk-channel-plugins/approvals/
- 来源：OpenClaw 官方文档原文（英文），MIT 许可，同步于 2026-10-11
- 官方原文：https://docs.openclaw.ai/zh-CN/plugins/sdk-channel-plugins/approvals

---
Most channel plugins do not need approval-specific code. Reach for this page
when yours does. Part of the [Building channel
plugins](https://funcoding.ai/agents/openclaw/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.
