---
summary: "Plan and audit checklist for moving Canvas out of core and into a bundled experimental plugin."
read_when:
  - Moving Canvas host, tools, commands, docs, or protocol ownership
  - Auditing whether Canvas is still core-owned
  - Preparing or reviewing the experimental Canvas plugin PR
title: "Canvas plugin refactor"
---

# Canvas plugin refactor

Canvas is low-use and experimental. Treat it as a bundled plugin, not a core feature. Core may keep generic gateway, node, HTTP, auth, config, and native-client plumbing, but Canvas-specific behavior should live under `extensions/canvas`.

## Goal

Move Canvas ownership to `extensions/canvas` while preserving the current paired-node behavior:

- the agent-facing `canvas` tool is registered by the Canvas plugin
- Canvas node commands are allowed only when the Canvas plugin registers them
- A2UI host/source files live under the Canvas plugin
- Canvas document materialization lives under the Canvas plugin
- CLI command implementation lives under the Canvas plugin, or delegates through a plugin-owned runtime barrel
- docs and plugin inventory describe Canvas as experimental and plugin-backed

## Non-goals

- Do not redesign the native app Canvas UI in this refactor.
- Do not remove Canvas protocol/client support from iOS, Android, or macOS unless a separate product decision says Canvas should be deleted.
- Do not build a broad plugin service framework only for Canvas unless at least one other bundled plugin needs the same seam.

## Current branch state

Done:

- Added bundled plugin package in `extensions/canvas`.
- Added `extensions/canvas/openclaw.plugin.json`.
- Moved the agent `canvas` tool from `src/agents/tools/canvas-tool.ts` to `extensions/canvas/src/tool.ts`.
- Removed core registration of `createCanvasTool` from `src/agents/openclaw-tools.ts`.
- Moved Canvas host implementation from `src/canvas-host` to `extensions/canvas/src/host`.
- Kept `extensions/canvas/runtime-api.ts` as the plugin-owned compatibility barrel for tests, packaging, and external public Canvas helpers.
- Moved Canvas document materialization from `src/gateway/canvas-documents.ts` to `extensions/canvas/src/documents.ts`.
- Moved Canvas CLI implementation and A2UI JSONL helpers into `extensions/canvas/src/cli.ts`.
- Moved Canvas host URL and scoped capability helpers into `extensions/canvas/src`.
- Moved Canvas node command defaults out of hardcoded core lists and into plugin `nodeInvokePolicies`.
- Added plugin-owned Canvas host config at `plugins.entries.canvas.config.host`.
- Moved Canvas and A2UI HTTP serving behind Canvas plugin HTTP route registration.
- Added generic plugin WebSocket upgrade dispatch for plugin-owned HTTP routes.
- Replaced Canvas-specific gateway host URL and node capability auth with generic hosted plugin surface and node capability helpers.
- Added plugin-owned hosted media resolvers so Canvas document URLs resolve through the Canvas plugin instead of core importing Canvas document internals.
- Added `api.registerNodeCliFeature(...)` so Canvas can declare `openclaw nodes canvas` as a plugin-owned node feature without manually spelling the parent command path.
- Removed production `src/**` imports of `extensions/canvas/runtime-api.js`.
- Moved the A2UI bundle source from `apps/shared/OpenClawKit/Tools/CanvasA2UI` to `extensions/canvas/src/host/a2ui-app`.
- Moved A2UI build/copy implementation under `extensions/canvas/scripts` and replaced root build wiring with generic bundled-plugin asset hooks.
- Removed the runtime legacy top-level `canvasHost` config alias.
- Kept the Canvas doctor migration so `openclaw doctor --fix` rewrites old `canvasHost` configs into `plugins.entries.canvas.config.host`.
- Removed old-agent Canvas protocol compatibility behind gateway protocol v4. Native clients and gateways now use only `pluginSurfaceUrls.canvas` plus `node.pluginSurface.refresh`; the deprecated `canvasHostUrl`, `canvasCapability`, and `node.canvas.capability.refresh` path is intentionally unsupported in this experimental refactor.
- Updated generated plugin inventory to include Canvas.
- Added plugin reference docs at `docs/plugins/reference/canvas.md`.

Known remaining core-owned Canvas surfaces:

- Native app Canvas handlers under `apps/` still intentionally consume the Canvas plugin surface
- native app Canvas protocol/client handlers under `apps/`
- published artifact output still uses `dist/canvas-host/a2ui` for backwards-compatible runtime lookup, but the copy step is now plugin-owned

## Target shape

`extensions/canvas` should own:

- plugin manifest and package metadata
- agent tool registration
- node invoke command policy
- Canvas host and A2UI runtime
- Canvas A2UI bundle source and asset build/copy scripts
- Canvas document creation and asset resolution
- Canvas CLI implementation
- Canvas docs page and plugin inventory entry

Core should own only generic seams:

- plugin discovery and registration
- generic agent tool registry
- generic node invoke policy registry
- generic gateway HTTP/auth and WebSocket upgrade dispatch
- generic hosted plugin surface URL resolution
- generic hosted media resolver registration
- generic node capability transport
- generic config plumbing
- generic bundled-plugin asset hook discovery

Native apps may keep Canvas command handlers as clients of the protocol. They are not the plugin runtime owner.

## Migration steps

1. Treat `plugins.entries.canvas.config.host` as the plugin-owned config surface.
2. Update docs so Canvas is described as an experimental bundled plugin.
3. Run focused Canvas tests, plugin inventory checks, plugin SDK API checks, and build/type gates affected by runtime boundaries.

## Audit checklist

Before calling the refactor complete:

- `rg "src/canvas-host|../canvas-host"` returns no live source imports.
- `rg "canvas-tool|createCanvasTool" src` finds no core-owned Canvas tool implementation.
- `rg "canvas.present|canvas.snapshot|canvas.a2ui" src/gateway` finds no hardcoded allowlist defaults outside generic plugin policy tests.
- `rg "extensions/canvas/runtime-api" src --glob '!**/*.test.ts'` is empty.
- `rg "canvas-documents" src` is empty.
- `rg "registerNodesCanvasCommands|nodes-canvas" src` is empty; the Canvas plugin registers `openclaw nodes canvas` through nested plugin CLI metadata.
- `rg "createCanvasHostHandler|handleA2uiHttpRequest" src/gateway` returns no gateway runtime ownership.
- `rg "apps/shared/OpenClawKit/Tools/CanvasA2UI|canvas-a2ui-copy|extensions/canvas/src/host/a2ui" scripts .github package.json` finds only compatibility wrappers or plugin-owned paths.
- `pnpm plugins:inventory:check` passes.
- `pnpm plugin-sdk:api:check` passes, or generated API contract records are intentionally updated and reviewed.
- Targeted Canvas tests pass.
- Changed-lanes tests pass for Canvas host/A2UI paths.
- PR body explicitly says Canvas is experimental and plugin-backed.

## Verification commands

Use targeted local checks while iterating:

```sh
pnpm test extensions/canvas/src/host/server.test.ts extensions/canvas/src/host/server.state-dir.test.ts extensions/canvas/src/host/file-resolver.test.ts
pnpm test src/gateway/server.plugin-node-capability-auth.test.ts src/gateway/server-import-boundary.test.ts
pnpm test extensions/canvas/src/config-migration.test.ts src/commands/doctor-legacy-config.migrations.test.ts
pnpm test test/scripts/changed-lanes.test.ts test/scripts/build-all.test.ts extensions/canvas/scripts/bundle-a2ui.test.ts test/scripts/bundled-plugin-assets.test.ts extensions/canvas/scripts/copy-a2ui.test.ts src/infra/run-node.test.ts
pnpm tsgo:extensions
pnpm plugins:inventory:check
pnpm plugin-sdk:api:check
```

Run `pnpm build` before push if runtime barrel, lazy import, packaging, or published plugin surfaces change.
