Skip to content

[Feature]: Expose canvas A2UI action bridge to web browsers (WebSocket fallback for window.openclawSendUserAction) #68497

Description

@n3wth

Context

The canvas host at /__openclaw__/canvas/ serves a test page (index.html inside the canvas-host dist and the default ~/.openclaw/canvas/index.html) with Hello / Time / Photo / Dalek buttons. The page status shows:

Bridge: missing · iOS=no · Android=no
Failed to send action: dalek

when opened in any desktop web browser.

Root cause

The test page checks for three bridges:

const hasIOS = () => !!(window.webkit?.messageHandlers?.openclawCanvasA2UIAction)
const hasAndroid = () => !!(window.openclawCanvasA2UIAction?.postMessage)
const hasHelper = () => typeof window.openclawSendUserAction === "function"

None are injected in a regular browser context, so every button falls through with Failed to send action: <name>. By design, the A2UI action path only works when the canvas is loaded inside an iOS/Android native OpenClaw node.

Request

Expose window.openclawSendUserAction in the web canvas bundle too, with the action routed back to the Gateway via the existing WebSocket (or a dedicated HTTP POST /__openclaw__/canvas/action). This would let the canvas be useful from:

  • canvas.newth.io style reverse-proxied deployments (behind Cloudflare Access / equivalent)
  • Desktop dev loops where the author doesn't want to round-trip through an iOS/Android node just to test a button
  • Embedded views (iframe from Control UI, dashboards, etc.)

Suggested shape

// Injected by canvas-host when running without a native bridge
window.openclawSendUserAction = ({ name, surfaceId, sourceComponentId, context }) => {
  const payload = { name, surfaceId, sourceComponentId, context, source: 'web' }
  // POST to same-origin endpoint; Gateway auth already established by browser
  fetch('/__openclaw__/canvas/action', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
    credentials: 'same-origin',
  }).then(r => r.ok ? dispatchStatus({ ok: true, id: payload.name })
                    : dispatchStatus({ ok: false, error: `HTTP ${r.status}` }))
  return true
}

Auth: the Gateway already guards /__openclaw__/ with the gateway token (verified: hitting the canvas URL without the Authorization: Bearer header returns 401). So the endpoint inherits auth for free.

Version

  • OpenClaw v2026.4.15
  • macOS Sonoma (arm64), served via openclaw gateway launchd agent
  • Reproduced in Chrome 147 and Safari 18

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions