openclaw - 💡(How to fix) Fix RFC: let plugins resolve their own `requireApproval` requests (extend contract with `onRegistered`) [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68626Fetched 2026-04-19 15:09:22
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

requireApproval lets a plugin request an approval but not resolve one. A plugin that serves its own dashboard via registerHttpRoute therefore can't unblock its own approvals from that UI — resolution has to come from Telegram / webchat / operator CLI.

Filing before a PR because the one-way shape is intentional per #55339 ("onResolution callback is informational only"). Want to check whether you'd welcome an additive extension before writing it.

Root Cause

Filing before a PR because the one-way shape is intentional per #55339 ("onResolution callback is informational only"). Want to check whether you'd welcome an additive extension before writing it.

Code Example

// src/plugins/hook-types.ts
requireApproval?: {
  /* …existing fields… */
  onRegistered?: (handle: {
    approvalId: string;
    resolve: (
      decision: PluginApprovalResolution,
      resolvedBy?: string
    ) => Promise<boolean>;
  }) => void;
};
RAW_BUFFERClick to expand / collapse

Summary

requireApproval lets a plugin request an approval but not resolve one. A plugin that serves its own dashboard via registerHttpRoute therefore can't unblock its own approvals from that UI — resolution has to come from Telegram / webchat / operator CLI.

Filing before a PR because the one-way shape is intentional per #55339 ("onResolution callback is informational only"). Want to check whether you'd welcome an additive extension before writing it.

Motivating use case

ClawLens is an observability plugin that gates high-risk tool calls with requireApproval and surfaces pending approvals in its own dashboard alongside risk context. Operators want to approve/deny there without context-switching to Telegram. Our attempt hit the wall: onResolution fires after plugin.approval.waitDecision has already returned (src/agents/pi-tools.before-tool-call.ts:220-234, 323), so it's observation-only by construction. Plugins can't call plugin.approval.resolve from inside themselves — the plugin API surface (src/plugins/types.ts:1890+) only exposes method registration, not invocation.

Full repro, audit logs, and gateway logs: https://github.com/nk3750/clawLens/issues/4

Proposal (additive, opt-in)

Extend the contract with an onRegistered callback that hands the plugin a bound resolver:

// src/plugins/hook-types.ts
requireApproval?: {
  /* …existing fields… */
  onRegistered?: (handle: {
    approvalId: string;
    resolve: (
      decision: PluginApprovalResolution,
      resolvedBy?: string
    ) => Promise<boolean>;
  }) => void;
};

Runtime change is ~10 lines in pi-tools.before-tool-call.ts, right after manager.register(): construct the closure, call approval.onRegistered?.(handle). The closure routes through the same plugin.approval.resolve path external surfaces use, so existing audit / forwarder / broadcast pathways stay authoritative. Single-winner semantics fall out of the existing double-resolve guard in ExecApprovalManager.resolve() (src/gateway/exec-approval-manager.ts:110-112).

No new gateway method, no new scope. Plugins that don't set onRegistered get identical behavior to today.

Authorization note

The closure is constructed by the runtime, handed only to the plugin that already declared the approval, and doesn't expose operator.approvals scope on the public plugin API. A plugin that wanted to abuse this could already decline to call requireApproval at all — so no new attack surface.

Ask

Would you welcome a PR along these lines? Scope I'd propose:

  1. Add onRegistered to the contract type (src/plugins/hook-types.ts).
  2. Wire pi-tools to invoke it after manager.register().
  3. Tests: happy-path resolve, double-resolve (loses to existing guard), onRegistered absent (no behavior change), plugin resolver racing an external channel.

If you'd rather a different shape (e.g., manager-side resolveByToolCallId + new gateway method), I'll write that instead — just want to align on shape before coding.

Related: #48515 frames approval routing as a general plugin primitive, of which this is a natural companion.

Happy to scope down further if there's a smaller first step that would help.

extent analysis

TL;DR

The proposed fix involves adding an onRegistered callback to the requireApproval contract, allowing plugins to resolve their own approvals.

Guidance

  • Review the proposed contract extension in src/plugins/hook-types.ts to ensure it aligns with the existing plugin API surface.
  • Verify that the runtime change in pi-tools.before-tool-call.ts correctly constructs the closure and calls approval.onRegistered?.(handle).
  • Consider the authorization implications of handing the plugin a bound resolver, as outlined in the authorization note.
  • Evaluate the test scope proposed, including happy-path resolve, double-resolve, and onRegistered absent scenarios.

Example

// src/plugins/hook-types.ts
requireApproval?: {
  /* …existing fields… */
  onRegistered?: (handle: {
    approvalId: string;
    resolve: (
      decision: PluginApprovalResolution,
      resolvedBy?: string
    ) => Promise<boolean>;
  }) => void;
};

Notes

The proposed change is additive and opt-in, meaning plugins that don't set onRegistered will retain their current behavior. The change does not introduce new gateway methods or scope.

Recommendation

Apply the proposed workaround by adding the onRegistered callback to the requireApproval contract, as it provides a clear and contained solution to the issue. This approach aligns with the existing plugin API surface and does not introduce new attack surfaces.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING