Summary
Make pending operator approvals durable, deep-linkable, and resolvable from any authorized surface, with first-answer-wins semantics and ancestor visibility for sub-agent requests.
Problem to solve
OpenClaw already has strong approval plumbing, but the operator experience is still tied to the surface that happened to receive the prompt:
- exec and plugin approvals live in process-local maps and disappear on gateway restart;
- resolved records survive only briefly, so a second surface cannot reliably show who won;
- Control UI approvals are modal-only rather than addressable by a stable URL;
- channel buttons encode approval decisions as
/approve ... command strings, forcing transports to infer product semantics from text;
- sub-agent approval events carry the child session key but are not projected into parent/orchestrator session views;
- timeout handling is split across manager timers and caller fallbacks instead of one durable fail-closed terminal state.
This fragments mobile and multi-surface operation. An operator may see an ask on Telegram, switch to the Control UI or phone app, and find no stable approval object to inspect or resolve.
Related asks cover parts of the same gap: #72545, #9987, #53250, #97043, and #101144.
Proposed solution
Introduce one gateway-owned operator approval lifecycle for exec and plugin/tool gates:
-
Durable canonical record
- Store one tagged
exec | plugin approval row in the shared state SQLite database.
- Persist a sanitized request projection, requester/reviewer identity, source session key, ancestor audience snapshot, deadline, terminal status, decision, resolver, and timestamps.
- Keep runtime execution bindings separate from the reviewer projection.
-
Atomic resolution
- Resolve with a SQLite compare-and-set over
status = pending and expires_at_ms > now.
- First terminal transition wins across every surface.
- Identical retries return the recorded winner; conflicting or late answers cannot overwrite it.
- Timeout, malformed verdicts, cancellation, and missing delivery routes become explicit deny/expired terminal outcomes.
-
Stable authenticated URL
- Give every approval a stable Control UI route such as
/approve/{approvalId}.
- Render a mobile-friendly standalone page with loading, pending, resolving, resolved-elsewhere, expired, forbidden, and error states.
- Keep credentials out of the URL. The ID is a locator, not a bearer capability; GET/resolve still require
operator.approvals and durable reviewer authorization.
-
Typed portable actions
- Add explicit presentation action variants for approval decisions and URL/Web App opens.
- Keep approval, command, callback, URL, and Web App actions distinguishable until each channel performs transport-private encoding.
- Preserve
/approve as a text fallback, not as the semantic button contract.
-
Ancestor visibility without cloned records
- Resolve a cycle-safe
source -> parent -> root session-key audience when the approval is created and persist that snapshot.
- Project requested and terminal events into parent/orchestrator views while resolving only the source approval record.
- Do not append model-visible transcript messages or wake parent agents merely to show operator state.
-
Incremental delivery
- PR 1: design note, SQLite repository, unified tagged lookup/resolve contract, atomic races, durable terminal tombstones.
- PR 2: standalone Control UI route/page and typed approval/URL actions.
- PR 3: native channel adoption (starting with Telegram), ancestor-session projection, and end-to-end multi-surface proof.
No new approval-mode config is proposed initially. Native one-click decisions can remain available while every prompt also exposes the same deep link.
Alternatives considered
- Keep the current in-memory managers and add only a UI route: cannot recover missed events, report a winner reliably, or make races durable.
- Clone one pending record per surface or ancestor: creates conflicting state and cleanup problems; projections should reference one canonical row.
- Make the URL an unauthenticated capability: leaks sensitive command/tool previews through copied URLs, browser history, and logs.
- Add channel-specific approval state: violates the transport-only channel boundary and makes first-answer-wins impossible to enforce centrally.
- Add an environment/config toggle matching the prior art: unnecessary for the first increment and adds another approval configuration surface.
Impact
Affected: operators using Control UI, Telegram, native/mobile clients, plugins, and sub-agent orchestration.
Severity: high for unattended or mobile-first workflows; a missed or stale prompt blocks the run and can leave the operator unsure whether an action executed.
Frequency: every approval is currently surface-local; reconnect and cross-surface races are intermittent but routine in multi-device operation.
Consequence: blocked turns, duplicated retries, stale buttons/modals, and poor visibility into child-agent gates.
Evidence/examples
Current OpenClaw already provides the right foundations: shared exec/plugin approval handlers, approval-scoped gateway clients, multi-target native channel fanout, pending-list replay, and first-answer-wins inside one process. The missing layer is durable shared state plus a stable reviewer surface and explicit action/event contracts.
Prior art: Omnigent's elicitation UX and ancestor mirroring at commit 46e3cd9754c3b8567f7b09f4d19b6249dabe0e80:
Omnigent's current active pending registry is process-local despite older persistence comments. OpenClaw should deliberately go further with shared-state SQLite compare-and-set semantics and restart-safe terminal records.
Additional information
Security and compatibility points for review:
- Existing
askFallback and plugin timeoutBehavior: allow paths conflict with strict fail-closed timeout semantics. The implementation should make the terminal outcome explicit and document/migrate any shipped fallback behavior rather than silently preserving it.
- Existing requester-device visibility is intentionally narrower than "any device with approval scope." Cross-surface authorization needs a durable operator/reviewer principal rule; session ancestry changes presentation only and must not widen authorization.
- A reverse-proxied gateway may not know its externally reachable Control UI origin. Relative links work inside the UI; channel delivery needs one gateway-authored advertised origin or a scoped native handoff, never transport-side URL guessing.
Summary
Make pending operator approvals durable, deep-linkable, and resolvable from any authorized surface, with first-answer-wins semantics and ancestor visibility for sub-agent requests.
Problem to solve
OpenClaw already has strong approval plumbing, but the operator experience is still tied to the surface that happened to receive the prompt:
/approve ...command strings, forcing transports to infer product semantics from text;This fragments mobile and multi-surface operation. An operator may see an ask on Telegram, switch to the Control UI or phone app, and find no stable approval object to inspect or resolve.
Related asks cover parts of the same gap: #72545, #9987, #53250, #97043, and #101144.
Proposed solution
Introduce one gateway-owned operator approval lifecycle for exec and plugin/tool gates:
Durable canonical record
exec | pluginapproval row in the shared state SQLite database.Atomic resolution
status = pendingandexpires_at_ms > now.Stable authenticated URL
/approve/{approvalId}.operator.approvalsand durable reviewer authorization.Typed portable actions
/approveas a text fallback, not as the semantic button contract.Ancestor visibility without cloned records
source -> parent -> rootsession-key audience when the approval is created and persist that snapshot.Incremental delivery
No new approval-mode config is proposed initially. Native one-click decisions can remain available while every prompt also exposes the same deep link.
Alternatives considered
Impact
Affected: operators using Control UI, Telegram, native/mobile clients, plugins, and sub-agent orchestration.
Severity: high for unattended or mobile-first workflows; a missed or stale prompt blocks the run and can leave the operator unsure whether an action executed.
Frequency: every approval is currently surface-local; reconnect and cross-surface races are intermittent but routine in multi-device operation.
Consequence: blocked turns, duplicated retries, stale buttons/modals, and poor visibility into child-agent gates.
Evidence/examples
Current OpenClaw already provides the right foundations: shared exec/plugin approval handlers, approval-scoped gateway clients, multi-target native channel fanout, pending-list replay, and first-answer-wins inside one process. The missing layer is durable shared state plus a stable reviewer surface and explicit action/event contracts.
Prior art: Omnigent's elicitation UX and ancestor mirroring at commit
46e3cd9754c3b8567f7b09f4d19b6249dabe0e80:Omnigent's current active pending registry is process-local despite older persistence comments. OpenClaw should deliberately go further with shared-state SQLite compare-and-set semantics and restart-safe terminal records.
Additional information
Security and compatibility points for review:
askFallbackand plugintimeoutBehavior: allowpaths conflict with strict fail-closed timeout semantics. The implementation should make the terminal outcome explicit and document/migrate any shipped fallback behavior rather than silently preserving it.