From d78512b09d1dd06fd89060c5edaf2d5c83467b1b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 7 Apr 2026 14:40:26 -0400 Subject: [PATCH] Refactor: centralize native approval lifecycle assembly (#62135) Merged via squash. Prepared head SHA: b7c20a7398e91fcba5a1cf6dc5dc0811547107b4 Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 4 + .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/channels/matrix.md | 18 +- docs/plugins/architecture.md | 7 +- docs/plugins/sdk-channel-plugins.md | 22 +- docs/plugins/sdk-migration.md | 28 + docs/plugins/sdk-overview.md | 2 + docs/tools/exec-approvals.md | 4 +- .../src/approval-handler.runtime.test.ts | 45 + .../discord/src/approval-handler.runtime.ts | 626 ++++++++++ .../discord/src/approval-native.test.ts | 52 +- extensions/discord/src/approval-native.ts | 76 +- extensions/discord/src/channel.ts | 1 + .../discord/src/monitor/exec-approvals.ts | 811 +------------ .../src/monitor/provider.lifecycle.test.ts | 66 +- .../discord/src/monitor/provider.lifecycle.ts | 13 - .../discord/src/monitor/provider.test.ts | 60 + extensions/discord/src/monitor/provider.ts | 42 +- .../src/test-support/provider.test-support.ts | 32 +- extensions/feishu/src/channel.ts | 2 +- extensions/googlechat/src/channel.ts | 2 +- extensions/matrix/src/approval-auth.ts | 2 +- .../src/approval-handler.runtime.test.ts | 46 + .../matrix/src/approval-handler.runtime.ts | 393 ++++++ extensions/matrix/src/approval-ids.ts | 6 + extensions/matrix/src/approval-native.test.ts | 108 +- extensions/matrix/src/approval-native.ts | 245 +++- extensions/matrix/src/channel.ts | 1 + .../matrix/src/exec-approval-resolver.test.ts | 53 +- .../matrix/src/exec-approval-resolver.ts | 27 +- .../matrix/src/exec-approvals-handler.test.ts | 556 --------- .../matrix/src/exec-approvals-handler.ts | 408 ------- extensions/matrix/src/exec-approvals.test.ts | 9 +- extensions/matrix/src/exec-approvals.ts | 127 +- extensions/matrix/src/matrix/monitor/index.ts | 18 +- .../matrix/monitor/reaction-events.test.ts | 70 +- .../src/matrix/monitor/reaction-events.ts | 15 +- extensions/mattermost/src/channel.ts | 2 +- extensions/msteams/src/channel.ts | 2 +- extensions/nextcloud-talk/src/channel.ts | 2 +- extensions/signal/src/channel.ts | 2 +- .../slack/src/approval-handler.runtime.ts | 331 +++++ extensions/slack/src/approval-native.test.ts | 28 +- extensions/slack/src/approval-native.ts | 44 +- extensions/slack/src/channel.ts | 1 + .../slack/src/monitor/exec-approvals.test.ts | 234 ---- .../slack/src/monitor/exec-approvals.ts | 393 ------ extensions/slack/src/monitor/provider.ts | 30 +- extensions/slack/src/monitor/types.ts | 2 + extensions/synology-chat/src/channel.ts | 2 +- .../telegram/src/approval-handler.runtime.ts | 188 +++ .../telegram/src/approval-native.test.ts | 67 ++ extensions/telegram/src/approval-native.ts | 30 +- extensions/telegram/src/channel.ts | 1 + .../telegram/src/exec-approval-resolver.ts | 41 +- .../src/exec-approvals-handler.test.ts | 528 -------- .../telegram/src/exec-approvals-handler.ts | 216 ---- .../telegram/src/monitor-polling.runtime.ts | 1 - .../telegram/src/monitor-webhook.runtime.ts | 1 - extensions/telegram/src/monitor.ts | 42 +- extensions/whatsapp/src/channel.setup.test.ts | 9 + extensions/whatsapp/src/channel.ts | 2 +- extensions/zalo/src/channel.ts | 2 +- package.json | 8 + scripts/lib/plugin-sdk-entrypoints.json | 2 + .../bash-tools.exec-host-shared.test.ts | 75 +- src/agents/bash-tools.exec-host-shared.ts | 25 +- ...ers.lifecycle.compaction-reconcile.test.ts | 1 + ...bedded-subscribe.handlers.messages.test.ts | 9 + ...pi-embedded-subscribe.handlers.messages.ts | 25 +- ...ded-subscribe.handlers.tools.media.test.ts | 1 + ...-embedded-subscribe.handlers.tools.test.ts | 1 + .../pi-embedded-subscribe.handlers.tools.ts | 10 +- .../pi-embedded-subscribe.handlers.types.ts | 2 + ...session.subscribeembeddedpisession.test.ts | 8 +- src/agents/pi-embedded-subscribe.ts | 2 + .../pi-tool-handler-state.test-helpers.ts | 1 + ...eply.directive.exec-agent-defaults.test.ts | 51 + src/auto-reply/reply/commands-approve.test.ts | 15 +- .../reply/get-reply-run.exec-hint.test.ts | 41 + src/auto-reply/reply/get-reply-run.ts | 31 + src/channels/plugins/approvals.test.ts | 75 +- src/channels/plugins/approvals.ts | 62 +- src/channels/plugins/types.adapters.ts | 44 +- src/channels/plugins/types.plugin.ts | 3 +- ...server-channels.approval-bootstrap.test.ts | 213 ++++ src/gateway/server-channels.test.ts | 70 +- src/gateway/server-channels.ts | 83 +- src/gateway/server-methods/exec-approval.ts | 12 + .../server-methods/server-methods.test.ts | 20 + src/infra/approval-gateway-resolver.test.ts | 143 +++ src/infra/approval-gateway-resolver.ts | 79 ++ src/infra/approval-handler-bootstrap.test.ts | 406 +++++++ src/infra/approval-handler-bootstrap.ts | 167 +++ src/infra/approval-handler-runtime.test.ts | 462 +++++++ src/infra/approval-handler-runtime.ts | 1072 +++++++++++++++++ src/infra/approval-native-delivery.ts | 7 +- .../approval-native-route-coordinator.test.ts | 212 ++++ .../approval-native-route-coordinator.ts | 392 ++++++ .../approval-native-route-notice.test.ts | 51 + src/infra/approval-native-route-notice.ts | 38 + src/infra/approval-native-runtime.test.ts | 822 ++++++++++++- src/infra/approval-native-runtime.ts | 302 ++--- src/infra/approval-native-target-key.test.ts | 31 + src/infra/approval-native-target-key.ts | 7 + src/infra/approval-view-model.ts | 219 ++++ src/infra/channel-approval-auth.test.ts | 27 +- src/infra/channel-approval-auth.ts | 1 + src/infra/channel-runtime-context.ts | 138 +++ src/infra/exec-approval-channel-runtime.ts | 14 +- src/infra/exec-approval-reply.ts | 25 +- .../exec-approval-session-target.test.ts | 63 +- src/infra/exec-approval-session-target.ts | 52 +- src/infra/exec-approval-surface.test.ts | 173 ++- src/infra/exec-approval-surface.ts | 33 +- src/infra/exec-approvals-effective.ts | 9 +- src/infra/exec-approvals-policy.test.ts | 30 +- .../approval-delivery-helpers.test.ts | 117 +- src/plugin-sdk/approval-delivery-helpers.ts | 172 ++- src/plugin-sdk/approval-handler-runtime.ts | 31 + src/plugin-sdk/approval-native-runtime.ts | 3 + src/plugin-sdk/approval-reply-runtime.ts | 3 + src/plugin-sdk/channel-runtime-context.ts | 6 + .../contracts/plugin-sdk-subpaths.test.ts | 2 +- src/plugins/runtime/runtime-channel.test.ts | 226 ++++ src/plugins/runtime/runtime-channel.ts | 163 +++ src/plugins/runtime/types-channel.ts | 33 + test/helpers/plugins/plugin-runtime-mock.ts | 11 + 128 files changed, 8839 insertions(+), 3995 deletions(-) create mode 100644 extensions/discord/src/approval-handler.runtime.test.ts create mode 100644 extensions/discord/src/approval-handler.runtime.ts create mode 100644 extensions/matrix/src/approval-handler.runtime.test.ts create mode 100644 extensions/matrix/src/approval-handler.runtime.ts create mode 100644 extensions/matrix/src/approval-ids.ts delete mode 100644 extensions/matrix/src/exec-approvals-handler.test.ts delete mode 100644 extensions/matrix/src/exec-approvals-handler.ts create mode 100644 extensions/slack/src/approval-handler.runtime.ts delete mode 100644 extensions/slack/src/monitor/exec-approvals.test.ts delete mode 100644 extensions/slack/src/monitor/exec-approvals.ts create mode 100644 extensions/telegram/src/approval-handler.runtime.ts delete mode 100644 extensions/telegram/src/exec-approvals-handler.test.ts delete mode 100644 extensions/telegram/src/exec-approvals-handler.ts create mode 100644 src/auto-reply/reply/get-reply-run.exec-hint.test.ts create mode 100644 src/gateway/server-channels.approval-bootstrap.test.ts create mode 100644 src/infra/approval-gateway-resolver.test.ts create mode 100644 src/infra/approval-gateway-resolver.ts create mode 100644 src/infra/approval-handler-bootstrap.test.ts create mode 100644 src/infra/approval-handler-bootstrap.ts create mode 100644 src/infra/approval-handler-runtime.test.ts create mode 100644 src/infra/approval-handler-runtime.ts create mode 100644 src/infra/approval-native-route-coordinator.test.ts create mode 100644 src/infra/approval-native-route-coordinator.ts create mode 100644 src/infra/approval-native-route-notice.test.ts create mode 100644 src/infra/approval-native-route-notice.ts create mode 100644 src/infra/approval-native-target-key.test.ts create mode 100644 src/infra/approval-native-target-key.ts create mode 100644 src/infra/approval-view-model.ts create mode 100644 src/infra/channel-runtime-context.ts create mode 100644 src/plugin-sdk/approval-handler-runtime.ts create mode 100644 src/plugin-sdk/channel-runtime-context.ts create mode 100644 src/plugins/runtime/runtime-channel.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a2ab6b6c4..6f6368dc372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,8 +87,12 @@ Docs: https://docs.openclaw.ai - Providers/Mistral: send `reasoning_effort` for `mistral/mistral-small-latest` (Mistral Small 4) with thinking-level mapping, and mark the catalog entry as reasoning-capable so adjustable reasoning matches Mistral’s Chat Completions API. (#62162) Thanks @neeravmakwana. - OpenAI TTS/Groq: send `wav` to Groq-compatible speech endpoints, honor explicit `responseFormat` overrides on OpenAI-compatible paths, and only mark voice-note output as voice-compatible when the actual format is `opus`. (#62233) Thanks @neeravmakwana. - BlueBubbles/network: respect explicit private-network opt-out for loopback and private `serverUrl` values across account resolution, status probes, monitor startup, and attachment downloads, while keeping public-host attachment hostname pinning intact. (#59373) Thanks @jpreagan. +<<<<<<< HEAD - Agents/heartbeat: keep heartbeat runs pinned to the main session so active subagent transcripts are not overwritten by heartbeat status messages. (#61803) thanks @100yenadmin. - Agents/compaction: stop compaction-wait aborts from re-entering prompt failover and replaying completed tool turns. (#62600) Thanks @i-dentifier. +======= +- Approvals/runtime: move native approval lifecycle assembly into shared core bootstrap/runtime seams driven by channel capabilities and runtime contexts, and remove the legacy bundled approval fallback wiring. (#62135) Thanks @gumadeiras. +>>>>>>> 367f6afaf1 (Approvals: finish capability cutover and Matrix parity) ## 2026.4.5 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 153de8b44d4..30463fa4e37 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -5e28885aeddb1c2e73040c88b503d584bbcd871c6941fd1ebf7f22ceac3477a6 plugin-sdk-api-baseline.json -c8bbc54b51588b6b9aecabb3fcf02ecb69867c8ac527b65d5ec3bc5c6288057a plugin-sdk-api-baseline.jsonl +7abdd7f9977c44bb8799f6c1047aa2a025217bbdc46c42329e46796e9d08b02a plugin-sdk-api-baseline.json +aca10bdd74bae01a8a2210c745ac2a0583b83ff8035aa2764b817967cb3a0b02 plugin-sdk-api-baseline.jsonl diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index dd5dfd2720a..ae8fed98ef8 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -880,7 +880,8 @@ See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layo ## Exec approvals -Matrix can act as an exec approval client for a Matrix account. +Matrix can act as a native approval client for a Matrix account. The native +DM/channel routing knobs still live under exec approval config: - `channels.matrix.execApprovals.enabled` - `channels.matrix.execApprovals.approvers` (optional; falls back to `channels.matrix.dm.allowFrom`) @@ -888,13 +889,14 @@ Matrix can act as an exec approval client for a Matrix account. - `channels.matrix.execApprovals.agentFilter` - `channels.matrix.execApprovals.sessionFilter` -Approvers must be Matrix user IDs such as `@owner:example.org`. Matrix auto-enables native exec approvals when `enabled` is unset or `"auto"` and at least one approver can be resolved, either from `execApprovals.approvers` or from `channels.matrix.dm.allowFrom`. Set `enabled: false` to disable Matrix as a native approval client explicitly. Approval requests otherwise fall back to other configured approval routes or the exec approval fallback policy. +Approvers must be Matrix user IDs such as `@owner:example.org`. Matrix auto-enables native approvals when `enabled` is unset or `"auto"` and at least one approver can be resolved. Exec approvals use `execApprovals.approvers` first and can fall back to `channels.matrix.dm.allowFrom`. Plugin approvals authorize through `channels.matrix.dm.allowFrom`. Set `enabled: false` to disable Matrix as a native approval client explicitly. Approval requests otherwise fall back to other configured approval routes or the approval fallback policy. -Native Matrix routing is exec-only today: +Matrix native routing now supports both approval kinds: -- `channels.matrix.execApprovals.*` controls native DM/channel routing for exec approvals only. -- Plugin approvals still use shared same-chat `/approve` plus any configured `approvals.plugin` forwarding. -- Matrix can still reuse `channels.matrix.dm.allowFrom` for plugin-approval authorization when it can infer approvers safely, but it does not expose a separate native plugin-approval DM/channel fanout path. +- `channels.matrix.execApprovals.*` controls the native DM/channel fanout mode for Matrix approval prompts. +- Exec approvals use the exec approver set from `execApprovals.approvers` or `channels.matrix.dm.allowFrom`. +- Plugin approvals use the Matrix DM allowlist from `channels.matrix.dm.allowFrom`. +- Matrix reaction shortcuts and message updates apply to both exec and plugin approvals. Delivery rules: @@ -910,9 +912,9 @@ Matrix approval prompts seed reaction shortcuts on the primary approval message: Approvers can react on that message or use the fallback slash commands: `/approve allow-once`, `/approve allow-always`, or `/approve deny`. -Only resolved approvers can approve or deny. Channel delivery includes the command text, so only enable `channel` or `both` in trusted rooms. +Only resolved approvers can approve or deny. For exec approvals, channel delivery includes the command text, so only enable `channel` or `both` in trusted rooms. -Matrix approval prompts reuse the shared core approval planner. The Matrix-specific native surface is transport only for exec approvals: room/DM routing and message send/update/delete behavior. +Matrix approval prompts reuse the shared core approval planner. The Matrix-specific native surface handles room/DM routing, reactions, and message send/update/delete behavior for both exec and plugin approvals. Per-account override: diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 8ee58b42e6f..a98b953d522 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -1134,6 +1134,7 @@ authoring plugins: `openclaw/plugin-sdk/channel-config-schema`, `openclaw/plugin-sdk/telegram-command-config`, `openclaw/plugin-sdk/channel-policy`, + `openclaw/plugin-sdk/approval-handler-runtime`, `openclaw/plugin-sdk/approval-runtime`, `openclaw/plugin-sdk/config-runtime`, `openclaw/plugin-sdk/infra-runtime`, @@ -1152,9 +1153,9 @@ authoring plugins: assistant-visible-text stripping, markdown render/chunking helpers, redaction helpers, directive-tag helpers, and safe-text utilities. - Approval-specific channel seams should prefer one `approvalCapability` - contract on the plugin. Core then reads approval auth, delivery, render, and - native-routing behavior through that one capability instead of mixing - approval behavior into unrelated plugin fields. + contract on the plugin. Core then reads approval auth, delivery, render, + native-routing, and lazy native-handler behavior through that one capability + instead of mixing approval behavior into unrelated plugin fields. - `openclaw/plugin-sdk/channel-runtime` is deprecated and remains only as a compatibility shim for older plugins. New code should import the narrower generic primitives instead, and repo code should not add new imports of the diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md index 0bfdc7dd85d..e858c3e7ced 100644 --- a/docs/plugins/sdk-channel-plugins.md +++ b/docs/plugins/sdk-channel-plugins.md @@ -60,22 +60,34 @@ Most channel plugins do not need approval-specific code. - Core owns same-chat `/approve`, shared approval button payloads, and generic fallback delivery. - Prefer one `approvalCapability` object on the channel plugin when the channel needs approval-specific behavior. +- `ChannelPlugin.approvals` is removed. Put approval delivery/native/render/auth facts on `approvalCapability`. +- `plugin.auth` is login/logout only; core no longer reads approval auth hooks from that object. - `approvalCapability.authorizeActorAction` and `approvalCapability.getActionAvailabilityState` are the canonical approval-auth seam. -- If your channel exposes native exec approvals, implement `approvalCapability.getActionAvailabilityState` even when the native transport lives entirely under `approvalCapability.native`. Core uses that availability hook to distinguish `enabled` vs `disabled`, decide whether the initiating channel supports native approvals, and include the channel in native-client fallback guidance. +- Use `approvalCapability.getActionAvailabilityState` for same-chat approval auth availability. +- 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. - 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.delivery` only for native approval routing or fallback suppression. +- Use `approvalCapability.nativeRuntime` for channel-owned native approval facts. Keep it lazy on hot channel entrypoints with `createLazyChannelApprovalNativeRuntimeAdapter(...)`, which can import your runtime module on demand while still letting core assemble the approval lifecycle. - Use `approvalCapability.render` only when a channel truly needs custom approval payloads instead of the shared renderer. - 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..accounts..execApprovals.*` instead of top-level defaults. - 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 a channel needs native approval delivery, keep channel code focused on target normalization and transport hooks. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, `createApproverRestrictedNativeApprovalCapability`, and `createChannelNativeApprovalRuntime` from `openclaw/plugin-sdk/approval-runtime` so core owns request filtering, routing, dedupe, expiry, and gateway subscription. +- 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 +- `observe` — optional delivery diagnostics hooks +- 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 now 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. - Different approval kinds can intentionally expose different native surfaces. Current bundled examples: - Slack keeps native approval routing available for both exec and plugin ids. - - Matrix keeps native DM/channel routing for exec approvals only and leaves - plugin approvals on the shared same-chat `/approve` path. + - Matrix keeps the same native DM/channel routing and reaction UX for exec + and plugin approvals, while still letting auth differ by approval kind. - `createApproverRestrictedNativeApprovalAdapter` still exists as a compatibility wrapper, but new code should prefer the capability builder and expose `approvalCapability` on the plugin. For hot channel entrypoints, prefer the narrower runtime subpaths when you only @@ -84,8 +96,10 @@ 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-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/setup-runtime`, `openclaw/plugin-sdk/setup-adapter-runtime`, diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 3b164d7bd2e..a200d7198e7 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -67,6 +67,32 @@ Current bundled provider examples: ## How to migrate + + Approval-capable channel plugins now expose native approval behavior through + `approvalCapability.nativeRuntime` plus the shared runtime-context registry. + + Key changes: + + - Replace `approvalCapability.handler.loadRuntime(...)` with + `approvalCapability.nativeRuntime` + - Move approval-specific auth/delivery off legacy `plugin.auth` / + `plugin.approvals` wiring and onto `approvalCapability` + - `ChannelPlugin.approvals` has been removed from the public channel-plugin + contract; move delivery/native/render fields onto `approvalCapability` + - `plugin.auth` remains for channel login/logout flows only; approval auth + hooks there are no longer read by core + - Register channel-owned runtime objects such as clients, tokens, or Bolt + apps through `openclaw/plugin-sdk/channel-runtime-context` + - Do not send plugin-owned reroute notices from native approval handlers; + core now owns routed-elsewhere notices from actual delivery results + - When passing `channelRuntime` into `createChannelManager(...)`, provide a + real `createPluginRuntime().channel` surface. Partial stubs are rejected. + + See `/plugins/sdk-channel-plugins` for the current approval capability + layout. + + + If your plugin uses `openclaw/plugin-sdk/windows-spawn`, unresolved Windows `.cmd`/`.bat` wrappers now fail closed unless you explicitly pass @@ -201,8 +227,10 @@ Current bundled provider examples: | `plugin-sdk/approval-auth-runtime` | Approval auth helpers | Approver resolution, same-chat action auth | | `plugin-sdk/approval-client-runtime` | Approval client helpers | Native exec approval profile/filter helpers | | `plugin-sdk/approval-delivery-runtime` | Approval delivery helpers | Native approval capability/delivery adapters | + | `plugin-sdk/approval-handler-runtime` | Approval handler helpers | Shared approval handler runtime helpers, including capability-driven native approval loading | | `plugin-sdk/approval-native-runtime` | Approval target helpers | Native approval target/account binding helpers | | `plugin-sdk/approval-reply-runtime` | Approval reply helpers | Exec/plugin approval reply payload helpers | + | `plugin-sdk/channel-runtime-context` | Channel runtime-context helpers | Generic channel runtime-context register/get/watch helpers | | `plugin-sdk/security-runtime` | Security helpers | Shared trust, DM gating, external-content, and secret-collection helpers | | `plugin-sdk/ssrf-policy` | SSRF policy helpers | Host allowlist and private-network policy helpers | | `plugin-sdk/ssrf-runtime` | SSRF runtime helpers | Pinned-dispatcher, guarded fetch, SSRF policy helpers | diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index cf8e0044f5d..1ffab25b4ca 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -151,6 +151,7 @@ explicitly promotes one as public. | `plugin-sdk/approval-auth-runtime` | Approver resolution and same-chat action-auth helpers | | `plugin-sdk/approval-client-runtime` | Native exec approval profile/filter helpers | | `plugin-sdk/approval-delivery-runtime` | Native approval capability/delivery adapters | + | `plugin-sdk/approval-handler-runtime` | Shared approval handler runtime helpers, including capability-driven native approval loading | | `plugin-sdk/approval-native-runtime` | Native approval target + account-binding helpers | | `plugin-sdk/approval-reply-runtime` | Exec/plugin approval reply payload helpers | | `plugin-sdk/command-auth-native` | Native command auth + native session-target helpers | @@ -172,6 +173,7 @@ explicitly promotes one as public. | --- | --- | | `plugin-sdk/runtime` | Broad runtime/logging/backup/plugin-install helpers | | `plugin-sdk/runtime-env` | Narrow runtime env, logger, timeout, retry, and backoff helpers | + | `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers | | `plugin-sdk/runtime-store` | `createPluginRuntimeStore` | | `plugin-sdk/plugin-runtime` | Shared plugin command/hook/http/interactive helpers | | `plugin-sdk/hook-runtime` | Shared webhook/internal hook pipeline helpers | diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index 965884dfd5f..39c335fe77a 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -557,8 +557,8 @@ Shared behavior: - Slack approvers can be explicit (`execApprovals.approvers`) or inferred from `commands.ownerAllowFrom` - Slack native buttons preserve approval id kind, so `plugin:` ids can resolve plugin approvals without a second Slack-local fallback layer -- Matrix native DM/channel routing is exec-only; Matrix plugin approvals stay on the shared - same-chat `/approve` and optional `approvals.plugin` forwarding paths +- Matrix native DM/channel routing and reaction shortcuts handle both exec and plugin approvals; + plugin authorization still comes from `channels.matrix.dm.allowFrom` - the requester does not need to be an approver - the originating chat can approve directly with `/approve` when that chat already supports commands and replies - native Discord approval buttons route by approval id kind: `plugin:` ids go diff --git a/extensions/discord/src/approval-handler.runtime.test.ts b/extensions/discord/src/approval-handler.runtime.test.ts new file mode 100644 index 00000000000..02c2e092b13 --- /dev/null +++ b/extensions/discord/src/approval-handler.runtime.test.ts @@ -0,0 +1,45 @@ +import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime"; +import { describe, expect, it } from "vitest"; +import { discordApprovalNativeRuntime } from "./approval-handler.runtime.js"; + +describe("discordApprovalNativeRuntime", () => { + it("routes origin approval updates to the Discord thread channel when threadId is present", async () => { + const prepared = await discordApprovalNativeRuntime.transport.prepareTarget({ + cfg: {} as never, + accountId: "main", + context: { + token: "discord-token", + config: {} as never, + }, + plannedTarget: { + surface: "origin", + reason: "preferred", + target: { + to: "123456789", + threadId: "777888999", + }, + }, + request: { + id: "req-1", + request: { + command: "hostname", + }, + createdAtMs: 0, + expiresAtMs: 1_000, + }, + approvalKind: "exec", + view: {} as never, + pendingPayload: {} as never, + }); + + expect(prepared).toEqual({ + dedupeKey: buildChannelApprovalNativeTargetKey({ + to: "123456789", + threadId: "777888999", + }), + target: { + discordChannelId: "777888999", + }, + }); + }); +}); diff --git a/extensions/discord/src/approval-handler.runtime.ts b/extensions/discord/src/approval-handler.runtime.ts new file mode 100644 index 00000000000..1a8ce69618d --- /dev/null +++ b/extensions/discord/src/approval-handler.runtime.ts @@ -0,0 +1,626 @@ +import { + Button, + Row, + Separator, + TextDisplay, + serializePayload, + type MessagePayloadObject, + type TopLevelComponents, +} from "@buape/carbon"; +import { ButtonStyle, Routes } from "discord-api-types/v10"; +import type { + ChannelApprovalCapabilityHandlerContext, + ExecApprovalExpiredView, + ExecApprovalPendingView, + ExecApprovalResolvedView, + PendingApprovalView, + PluginApprovalExpiredView, + PluginApprovalPendingView, + PluginApprovalResolvedView, +} from "openclaw/plugin-sdk/approval-handler-runtime"; +import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime"; +import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime"; +import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import type { + ExecApprovalActionDescriptor, + ExecApprovalDecision, +} from "openclaw/plugin-sdk/infra-runtime"; +import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime"; +import { shouldHandleDiscordApprovalRequest } from "./approval-native.js"; +import { isDiscordExecApprovalClientEnabled } from "./exec-approvals.js"; +import { createDiscordClient, stripUndefinedFields } from "./send.shared.js"; +import { DiscordUiContainer } from "./ui.js"; + +type PendingApproval = { + discordMessageId: string; + discordChannelId: string; +}; +type DiscordPendingDelivery = { + body: ReturnType; +}; +type PreparedDeliveryTarget = { + discordChannelId: string; + recipientUserId?: string; +}; + +export type DiscordApprovalHandlerContext = { + token: string; + config: DiscordExecApprovalConfig; +}; + +function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): { + accountId: string; + context: DiscordApprovalHandlerContext; +} | null { + const context = params.context as DiscordApprovalHandlerContext | undefined; + const accountId = params.accountId?.trim() || ""; + if (!context?.token || !accountId) { + return null; + } + return { accountId, context }; +} + +class ExecApprovalContainer extends DiscordUiContainer { + constructor(params: { + cfg: OpenClawConfig; + accountId: string; + title: string; + description?: string; + commandPreview: string; + commandSecondaryPreview?: string | null; + metadataLines?: string[]; + actionRow?: Row