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