Canvas apps
F20 · 24 mapped behaviors in the Self-Healing Codebase Project. Follow the prerequisites, actions and expected results; expand the original CLI recipe to replay it.
778 behavior IDs · 28 families · 844 named recipes & support steps
16 October 2026 · PR #18334 at 9a3c3a69
Canvas 1.27.1 · Agent Server 1.55.0 · automation 1.20.0
This is the map at the revision above, including its open bug notes. It does not incorporate later changes on main or claim a fresh verification run. Setup & reading guide · Download the scenario inventory
All scenarios & marker key · Open a script to read Given / Do / Expect. Expand the exact recipe for replayable commands.
No scenarios match. Try a different word or reset.
F20 Canvas apps
24 behaviors · 28 scripts & contracts · Maintained source
Entry points & prerequisites
What this family covers
Canvas apps (Canvas Extensions) are trusted browser modules installed on the active local Agent Server. Under Customize → Apps a user installs an app from a local path or a Git source (it arrives disabled), reviews its card, enables it through a trust confirmation, updates, disables or uninstalls it, and opens the pages it contributes from the main sidebar rail at /extensions/<name>/<path>.
How a user gets here
- Main sidebar Customize (
sidebar-skills-link, lands on/mcp), then Apps in the Customize navigation (sidebar-extensions-/apps). - Command menu (
Control+k/Meta+korcommand-menu-trigger): searchCustomize, Enter (lands on/mcp), then Apps. SearchingAppsfinds nothing. - Phone:
/customizeshows the Customize hub (extensions-mobile-hub) instead of redirecting; tap Apps. - Direct URL
/apps. - App pages are not in the command menu; only the rail (or a URL) reaches them.
- App pages: the app's entry in the main sidebar rail (phone: the sidebar drawer,
sidebar-mobile-menu-toggle), or the direct URL/extensions/<name>/<path>[/<sub-path>].
Given
Preconditions:
- Baseline state (launched, doctored,
onboard --skipdone) on the launcher's local Agent Server. No LLM is needed. - No app is installed:
control-openhands api GET /api/canvas-extensions/installedreturns"canvas_extensions": []. - Run commands from the checkout root: recipes install the bundled, reviewed fixture
$PWD/src/fixtures/canvas-extensions/demo-page(namedemo-page, display nameDemo page, one pagehelloat/hello). Never install or enable arbitrary third-party code: enabling runs it inside Canvas. - Git bullets (tree URL, Ref and Path, busy lock, duplicate): the Agent Server can read
https://github.com/OpenHands/OpenHandsanonymously (read-only; no account needed), and that repo'smainholds the same reviewed fixture atsrc/fixtures/canvas-extensions/demo-page. Check both before installing, with a read-only shell step:git clone -q --depth 1 --filter=blob:none --sparse https://github.com/OpenHands/OpenHands "$OH_VERIFY_RUN/private/qa-upstream" && git -C "$OH_VERIFY_RUN/private/qa-upstream" sparse-checkout set src/fixtures/canvas-extensions && diff -r "$OH_VERIFY_RUN/private/qa-upstream/src/fixtures/canvas-extensions/demo-page" src/fixtures/canvas-extensions/demo-page && git -C "$OH_VERIFY_RUN/private/qa-upstream" rev-parse HEADmust print no diff and a commit. Enable a Git-installed copy only when the card's Ref equals that commit. If GitHub is unreachable or the diff is not empty, these bullets are blocked (a local smart-HTTP Git server would do, butcontrol-openhandshas no verb for one; dumb HTTP such aspython3 -m http.serverfails the Agent Server's fetch). - Desktop viewport unless a bullet says otherwise.
Sidebar entry and empty pageF20.entry-points · F20.pageMapped behavior
Scenario F20--sidebar-entry-and-empty-page · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Sidebar skills”.
- DoChoose “Sidebar extensions /apps”.
Expect
the Apps page is reached from the sidebar Customize link (lands on /mcp) then Apps in the Customize navigation, from the command menu's Customize command plus Apps, from the phone Customize hub, and by direct URL /apps; there is no bare /extensions page and no "Apps" command.
/apps shows the title Apps for Agent Canvas, a description, a Build an app docs link (new tab), an enabled Add app button, the amber trust notice, the Installed apps heading and, with nothing installed, No apps are installed on this backend.
Exact CLI recipe & bug checks
From / run control-openhands browser click 'testid=sidebar-skills-link' --expect-url '/mcp(\?|$)', control-openhands browser click 'testid=sidebar-extensions-/apps' --expect-url '/apps(\?|$)', then control-openhands browser snapshot 'testid=canvas-extensions-screen >> main'. The snapshot shows heading Apps for Agent Canvas, the description Apps are built with the Canvas Extensions API, link Build an app, button Add app, the trust notice, heading Installed apps and paragraph No apps are installed on this backend. control-openhands browser enabled 'testid=canvas-extensions-add-button' is true and control-openhands browser attr 'role=link[name="Build an app"]' target is _blank. control-openhands browser screenshot --feature F20.page --name empty.
Add form and its closersF20.add-modalMapped behavior
Scenario F20--add-form-and-its-closers · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /apps.
- DoChoose “Canvas extensions add”.
- DoEnter “ ” in “Add canvas extension source”.
- DoChoose “Add canvas extension dismiss”.
- DoChoose “Add canvas extension modal close”.
- DoPress Escape.
- DoUse browser mouse click as specified in the exact recipe.
Expect
Add app opens a form with App source (required), Ref and Path; Install stays disabled while the source is empty or blank; Close, the X, Escape and a backdrop click close it.
Exact CLI recipe & bug checks
On /apps (control-openhands browser goto /apps; the previous bullet ends on the /extensions not-found page) run control-openhands browser click 'testid=canvas-extensions-add-button' and control-openhands browser snapshot 'testid=add-canvas-extension-modal': heading Add app, intro Install from a Git source or a path on the active Agent Server. The app will be installed disabled., textboxes App source, Ref Optional, Path Optional, buttons Close and Install [disabled]. After control-openhands browser fill 'testid=add-canvas-extension-source-input' ' ', control-openhands browser enabled 'testid=add-canvas-extension-submit' is still false. Each of control-openhands browser click 'testid=add-canvas-extension-dismiss', control-openhands browser click 'testid=add-canvas-extension-modal-close', control-openhands browser press Escape and control-openhands browser mouse-click 20 500 (backdrop) closes it: control-openhands browser count 'testid=add-canvas-extension-modal' is 0 (reopen with canvas-extensions-add-button between them).
Install error, local pathF20.install-errorMapped behavior
Scenario F20--install-error-local-path · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “/nonexistent/qa-app” in “Add canvas extension source”.
- DoChoose “Add canvas extension submit”.
Expect
a failed install toasts the server's reason (Could not read canvas extension source: …) and keeps the form open with the input, for local paths and for unreachable Git sources alike.
Exact CLI recipe & bug checks
Open the form, run control-openhands browser fill 'testid=add-canvas-extension-source-input' /nonexistent/qa-app, control-openhands browser click 'testid=add-canvas-extension-submit', then control-openhands browser toasts: Could not read canvas extension source: Local extension path does not exist: /nonexistent/qa-app. browser count 'testid=add-canvas-extension-modal' stays 1 and the input keeps its value. control-openhands browser errors --app-only lists the expected 400 on POST /api/canvas-extensions/install.
Install error, Git sourceF20.install-errorMapped behavior
Scenario F20--install-error-git-source · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “http://127.0.0.1:9/qa-owner/qa-missing” in “Add canvas extension source”.
- DoChoose “Add canvas extension submit”.
- WaitWait for “Add canvas extension submit” / “Install” to appear.
Expect
a failed install toasts the server's reason (Could not read canvas extension source: …) and keeps the form open with the input, for local paths and for unreachable Git sources alike.
Exact CLI recipe & bug checks
In the same form run control-openhands browser toasts --clear, control-openhands browser fill 'testid=add-canvas-extension-source-input' 'http://127.0.0.1:9/qa-owner/qa-missing', control-openhands browser click 'testid=add-canvas-extension-submit', control-openhands browser wait 'testid=add-canvas-extension-submit >> text=Install' --timeout 60000, then control-openhands browser toasts --history and control-openhands browser screenshot --feature F20.install-error --name git-server-reason. The history holds one toast with the server's 400 detail, Could not read canvas extension source: Failed to fetch extension from http://127.0.0.1:9/qa-owner/qa-missing (browser toasts may still list the local-path toast below it). The form stays open with the URL.
Install the demoF20.installMapped behavior
Scenario F20--install-the-demo · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “$PWD/src/fixtures/canvas-extensions/demo-page” in “Add canvas extension source”.
- DoChoose “Add canvas extension submit”.
- DoReload the page.
Expect
installing from a local path shows Installing…, toasts App installed. Review it here, then enable it when you are ready., closes the form and lists the app disabled (after reload too) with no rail entry; a local App source plus Path is joined into one path.
Exact CLI recipe & bug checks
In the open form run control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions/demo-page" and control-openhands browser click 'testid=add-canvas-extension-submit' --observe 'testid=add-canvas-extension-submit': the observed states are Install, Installing… [disabled], <absent>. control-openhands browser toasts shows App installed. Review it here, then enable it when you are ready. Then control-openhands browser reload, control-openhands browser attr 'testid=canvas-extension-card-demo-page >> role=switch' aria-checked (false) and control-openhands browser count 'testid=sidebar-canvas-extension-demo-page-hello' (0).
CardF20.cardMapped behavior
Scenario F20--card · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoRun
control-openhands browser snapshot 'testid=canvas-extension-card-demo-page': headingDemo page, the source path, switchEnable,A dependency-free fixture for the Canvas Extension page ABI., textDisabled v0.1.0 Pages: 1, list itemHello from an extension, buttonsUpdateandUninstall.control-openhands browser screenshot --feature F20.card --name disabled.
Expect
each card shows the display name, source, a role=switch (Enable/Disable), the description, Disabled/Enabled, v<version> and Pages: N pills, each page title, and Update and Uninstall buttons.
Exact CLI recipe & bug checks
Run control-openhands browser snapshot 'testid=canvas-extension-card-demo-page': heading Demo page, the source path, switch Enable, A dependency-free fixture for the Canvas Extension page ABI., text Disabled v0.1.0 Pages: 1, list item Hello from an extension, buttons Update and Uninstall. control-openhands browser screenshot --feature F20.card --name disabled.
Trust confirmationF20.enable-confirmMapped behavior
Scenario F20--trust-confirmation · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extension card demo page” / the switch.
- DoChoose “Confirmation dialog” / “Cancel”.
- DoUse browser focus as specified in the exact recipe.
- DoPress Space.
- DoPress Escape.
- DoUse browser mouse click as specified in the exact recipe.
- DoChoose “Confirmation dialog” / “Confirm”.
- DoReload the page.
Expect
switching a disabled app on (click or Space) opens a trust confirmation (confirmation-modal) with Cancel and Enable trusted app; Cancel, Escape and a backdrop click leave it disabled, confirming enables it (persists after reload).
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=canvas-extension-card-demo-page >> role=switch' and control-openhands browser snapshot 'testid=confirmation-modal': the paragraph This app runs trusted JavaScript inside Agent Canvas and can make authenticated requests to the active Agent Server. Review its source and revision before enabling it., buttons Cancel and Enable trusted app. control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button' closes it and aria-checked stays false. Keyboard: control-openhands browser focus 'testid=canvas-extension-card-demo-page >> role=switch' and control-openhands browser press Space open it again (browser count 'testid=confirmation-modal' is 1); control-openhands browser press Escape closes it and api GET /api/canvas-extensions/installed still says "enabled": false. Backdrop: click the switch again, control-openhands browser mouse-click 20 500, then browser count 'testid=confirmation-modal' is 0 and the API still says "enabled": false. Reopen with the click, then control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button', control-openhands browser reload and read aria-checked: true; control-openhands browser text 'testid=canvas-extension-card-demo-page' contains Enabled.
Rail entryF20.rail-entry · F20.rail-labelMapped behavior
Scenario F20--rail-entry · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- WaitWait for “Sidebar canvas extension demo page hello” to appear.
Expect
an enabled app's page appears in the main sidebar rail as sidebar-canvas-extension-<name>-<pageId> linking to /extensions/<name>/<path>.
the rail entry is labelled with the page's nav_label (Extension demo for the demo fixture), falling back to its title.
Exact CLI recipe & bug checks
After enabling, run control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --timeout 10000, control-openhands browser attr 'testid=sidebar-canvas-extension-demo-page-hello' href (/extensions/demo-page/hello) and control-openhands browser text 'testid=sidebar-canvas-extension-demo-page-hello'. Expected Extension demo (the fixture's nav_label); today it reads Hello from an extension (see Gotchas).
Render the pageF20.page-renderMapped behavior
Scenario F20--render-the-page · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Sidebar canvas extension demo page hello”.
- DoOpen /extensions/demo-page/hello/nested.
Expect
the rail entry and the direct URL mount the app into main named after the page title (demo: h1 Hello from a Canvas Extension); a deeper URL passes the rest of the path to the app (Nested extension path: nested).
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)' and control-openhands browser snapshot 'role=main[name="Hello from an extension"]': heading Hello from a Canvas Extension and paragraph Host API 1 on backend default-local. Direct URL: control-openhands browser goto /extensions/demo-page/hello/nested and the same snapshot show the paragraph Nested extension path: nested. control-openhands browser screenshot --feature F20.page-render --name rail.
App backend viewF20.app-backend-viewKnown issue
Scenario F20--app-backend-view · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /extensions/demo-page/hello.
Expect
an app page learns from the host whether it has a backend view; the host offers one only when the Agent Server advertises the canvas_app_backend_bridge_v1 capability with an app-backend ingress URL, so on the pinned server the demo's status line reads App backend view unavailable on every path of its page.
Linked known issues
Known issue OpenHands/software-agent-sdk#5648 (F20.app-backend-view): app-backend WebSocket subprotocols are reported dropped. Recheck on a Linux Agent Server with configured public ingress and reviewed platform-matching backend artifacts, using a genuine admitted app-backend session and subprotocol negotiation. SDK 1.55.0 supports only Linux app-backend artifacts, so a native macOS server cannot drive this branch. A default demo without the bridge capability cannot exercise the proxied WebSocket path.
Known issue OpenHands/software-agent-sdk#5935 (F20.app-backend-view): an explicit OH_APP_BACKEND_PUBLIC_URL_IS_NONE=1 does not clear a bridge URL set in the Agent Server JSON configuration. Canvas can therefore be offered an app-backend view at the unwanted configured ingress. Recheck an owned bridge-capable server with a JSON public URL and that environment-null override, then inspect /server_info and the real app page; the default launcher has neither conflicting configuration source.
Known issue OpenHands/software-agent-sdk#5914 (F20.app-backend-view): app-backend HTTP responses can carry both the backend's and the Agent Server's Date and Server headers, producing an invalid joined date and disclosing the backend stack. Recheck a reviewed HTTP app backend on a supported Linux Agent Server that sends these headers through a real admitted app-backend session. A default demo page without the bridge capability or a direct backend request cannot establish the proxied response.
Exact CLI recipe & bug checks
Still on /extensions/demo-page/hello/nested (where Render the page ends) run control-openhands browser text 'testid=demo-extension-app-backend-status', control-openhands api GET /server_info --pick capabilities and control-openhands api GET /server_info --pick app_backend_ingress_url. The host hands a page appBackendView only when the capabilities list canvas_app_backend_bridge_v1 and the ingress URL is not null: with the pinned server (1.53.0: neither) the line reads App backend view unavailable; a server advertising both would make the demo read App backend view available (not driven here). control-openhands browser goto /extensions/demo-page/hello shows the same line, control-openhands browser snapshot 'role=main[name="Hello from an extension"]' lists it as the last paragraph, and control-openhands browser screenshot --feature F20.app-backend-view --name unavailable.
Active and collapsed rail entryF20.rail-activeMapped behavior
Scenario F20--active-and-collapsed-rail-entry · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /apps.
- DoChoose “Sidebar canvas extension demo page hello”.
- DoChoose “Sidebar collapse toggle”.
- DoUse browser tooltip as specified in the exact recipe.
- DoChoose “Sidebar collapse toggle”.
- DoOpen /.
- DoPress Control+k.
- DoEnter “Hello” in “Command menu” / the combobox.
- DoPress Escape.
Expect
on the app's page its rail entry is marked current (aria-current="page", highlighted); with the sidebar collapsed the entry is an icon whose tooltip and aria-label carry the label. App pages are not listed in the command menu.
Exact CLI recipe & bug checks
With the demo enabled, run control-openhands browser goto /apps; control-openhands browser attr 'testid=sidebar-canvas-extension-demo-page-hello' aria-current is null; after control-openhands browser click 'testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)' it is page. Run control-openhands browser click 'testid=sidebar-collapse-toggle': browser visible 'testid=sidebar-canvas-extension-demo-page-hello' stays true (about 40 px wide), browser attr ... aria-label and control-openhands browser tooltip 'testid=sidebar-canvas-extension-demo-page-hello' read Hello from an extension; control-openhands browser screenshot --feature F20.rail-active --name collapsed shows the highlighted icon. Expand again with browser click 'testid=sidebar-collapse-toggle' (its aria-label is Collapse sidebar again). Command menu: browser goto /, browser press Control+k, browser type 'testid=command-menu >> role=combobox' Hello and browser snapshot 'testid=command-menu' read No commands found; browser press Escape.
PhoneF20.phoneMapped behavior
Scenario F20--phone · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupUse the phone viewport.
- DoOpen /customize.
- DoChoose “Extensions mobile hub” / “Sidebar extensions /apps”.
- DoChoose “Sidebar mobile menu toggle”.
- DoChoose “Sidebar mobile drawer” / “Sidebar canvas extension demo page hello”.
- SetupUse the desktop viewport.
Expect
at 390 px the Customize hub lists Apps, the Apps page, the Add form and an app page fit without horizontal overflow, and the app's entry is in the sidebar drawer.
Exact CLI recipe & bug checks
Run control-openhands browser viewport phone, control-openhands browser goto /customize (the URL stays /customize), control-openhands browser click 'testid=extensions-mobile-hub >> testid=sidebar-extensions-/apps' --expect-url '/apps(\?|$)', control-openhands browser bbox 'testid=canvas-extensions-screen' (insideViewport true, pageHorizontalOverflow false) and control-openhands browser screenshot --feature F20.phone --name apps. Open the form with canvas-extensions-add-button; control-openhands browser bbox 'testid=add-canvas-extension-modal' is inside the viewport with no page overflow; close it with add-canvas-extension-dismiss. Then control-openhands browser click 'testid=sidebar-mobile-menu-toggle', control-openhands browser visible 'testid=sidebar-mobile-drawer >> testid=sidebar-canvas-extension-demo-page-hello' (true), control-openhands browser click 'testid=sidebar-mobile-drawer >> testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)' and control-openhands browser bbox 'role=main[name="Hello from an extension"]' (inside, no overflow). Return with control-openhands browser viewport desktop.
UpdateF20.refreshMapped behavior
Scenario F20--update · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /apps.
- DoChoose “Canvas extension refresh demo page”.
Expect
Update re-installs from the recorded source, toasts App updated. and keeps the enabled state; a failed update toasts the error and leaves the card unchanged.
Exact CLI recipe & bug checks
Note installed_at from control-openhands api GET /api/canvas-extensions/installed, then on /apps (control-openhands browser goto /apps; Phone ends on the extension page) run control-openhands browser click 'testid=canvas-extension-refresh-demo-page' and control-openhands browser toasts (App updated.). The API shows a newer installed_at and keeps "enabled": true; browser count 'testid=sidebar-canvas-extension-demo-page-hello' stays 1.
Survives a restartF20.persist-restartMapped behavior
Scenario F20--survives-a-restart · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoRestart the run-owned stack as specified.
- DoOpen /.
- WaitWait for “Sidebar canvas extension demo page hello” to appear.
- DoOpen /apps.
Expect
installed apps, their enabled state and rail entries survive a stack restart.
Exact CLI recipe & bug checks
Run control-openhands restart, control-openhands doctor, control-openhands browser goto /, control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --timeout 15000, then control-openhands browser goto /apps and read the switch's aria-checked: true.
Update with the backend downF20.refreshMapped behavior
Scenario F20--update-with-the-backend-down · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupChange the run-owned service state: stop agent-server.
- DoChoose “Canvas extension refresh demo page”.
- DoRestart the run-owned stack as specified.
Expect
Update re-installs from the recorded source, toasts App updated. and keeps the enabled state; a failed update toasts the error and leaves the card unchanged.
Exact CLI recipe & bug checks
On /apps run control-openhands service stop agent-server, control-openhands browser click 'testid=canvas-extension-refresh-demo-page' and control-openhands browser toasts: one toast, An error occurred (the ingress answers the Update's POST /api/canvas-extensions/install with a 502 that carries no server reason; see Gotchas), and the card still shows Enabled (browser screenshot --feature F20.refresh --name backend-down). Bring the stack back with control-openhands restart and control-openhands doctor before going on.
DisableF20.disableMapped behavior
Scenario F20--disable · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extension card demo page” / the switch.
- WaitWait for “Sidebar canvas extension demo page hello” to disappear.
- DoReload the page.
- DoOpen /extensions/demo-page/hello.
Expect
switching an enabled app off happens immediately without confirmation, removes its rail entry and makes its page unavailable.
Exact CLI recipe & bug checks
On /apps run control-openhands browser click 'testid=canvas-extension-card-demo-page >> role=switch'; control-openhands browser count 'testid=confirmation-modal' is 0 and control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --state detached --timeout 10000 succeeds. After control-openhands browser reload the switch's aria-checked is false; control-openhands browser goto /extensions/demo-page/hello and browser snapshot 'role=main' show Not available.
UninstallF20.uninstallMapped behavior
Scenario F20--uninstall · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /apps.
- WaitWait for “Sidebar canvas extension demo page hello” to appear.
- DoChoose “Canvas extension uninstall demo page”.
- DoPress Escape.
- DoChoose “Confirmation dialog” / “Cancel”.
- DoChoose “Confirmation dialog” / “Confirm”.
- WaitWait for “Sidebar canvas extension demo page hello” to disappear.
- DoReload the page.
Expect
Uninstall asks Uninstall <display name> from this backend?; Escape or Cancel keep the app, Confirm toasts App uninstalled. and removes the card and the rail entry.
Exact CLI recipe & bug checks
Re-enable the demo on /apps (control-openhands browser goto /apps, since Disable ends on the extension page; the switch, then testid=confirmation-modal >> testid=confirm-button, then browser wait 'testid=sidebar-canvas-extension-demo-page-hello'). Run control-openhands browser click 'testid=canvas-extension-uninstall-demo-page' and control-openhands browser snapshot 'testid=confirmation-modal': Uninstall Demo page from this backend?, buttons Cancel and Confirm. control-openhands browser press Escape leaves browser count 'testid=canvas-extension-card-demo-page' at 1; so does clicking Uninstall again and control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button'. Click Uninstall again, control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button' and control-openhands browser toasts (App uninstalled.); control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --state detached --timeout 10000 succeeds. After control-openhands browser reload the card count is 0, the page shows No apps are installed on this backend., and /extensions/demo-page/hello shows Not available.
Local source plus PathF20.installMapped behavior
Scenario F20--local-source-plus-path · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /apps.
- DoChoose “Canvas extensions add”.
- DoEnter “$PWD/src/fixtures/canvas-extensions” in “Add canvas extension source”.
- DoEnter “demo-page” in “Add canvas extension repo path”.
- DoChoose “Add canvas extension submit”.
- WaitWait for “Add canvas extension modal” to disappear.
Expect
installing from a local path shows Installing…, toasts App installed. Review it here, then enable it when you are ready., closes the form and lists the app disabled (after reload too) with no rail entry; a local App source plus Path is joined into one path.
Exact CLI recipe & bug checks
Run control-openhands browser goto /apps (the Uninstall check ends on /extensions/demo-page/hello), control-openhands browser click 'testid=canvas-extensions-add-button', control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions", control-openhands browser fill 'testid=add-canvas-extension-repo-path-input' demo-page, control-openhands browser click 'testid=add-canvas-extension-submit' and control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 30000. The toast is App installed. … and api GET /api/canvas-extensions/installed shows "source" ending in /src/fixtures/canvas-extensions/demo-page with "repo_path": null. Uninstall it as above (switch untouched, so no rail check).
Git tree URLF20.git-tree-urlMapped behavior
Scenario F20--git-tree-url · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extensions add”.
- DoEnter “https://github.com/OpenHands/OpenHands/tree/main/src/fixtures/canvas-extensions/demo-page” in “Add canvas extension source”.
- DoChoose “Add canvas extension submit”.
- WaitWait for “Add canvas extension modal” to disappear.
- DoReload the page.
Expect
a Git host folder URL (…/<owner>/<repo>/tree/<ref>/<path>) pasted into App source is split into source, ref and path; the card shows the repo URL as source, Ref (resolved commit) and Path.
Exact CLI recipe & bug checks
Needs the Git precondition (upstream reachable, fixture identical). Run control-openhands browser click 'testid=canvas-extensions-add-button', control-openhands browser fill 'testid=add-canvas-extension-source-input' 'https://github.com/OpenHands/OpenHands/tree/main/src/fixtures/canvas-extensions/demo-page' (Ref and Path empty), control-openhands browser click 'testid=add-canvas-extension-submit' --observe 'testid=add-canvas-extension-submit' (Installing… [disabled] for about 0.4 to 2 s, then <absent>) and control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 90000; the toast is App installed. …. After control-openhands browser reload, control-openhands browser snapshot 'testid=canvas-extension-card-demo-page' shows source https://github.com/OpenHands/OpenHands, switch Enable, term Ref with the 40-character commit from the precondition and term Path with src/fixtures/canvas-extensions/demo-page; api GET /api/canvas-extensions/installed has the same source, resolved_ref and repo_path. control-openhands browser screenshot --feature F20.git-tree-url --name card.
Duplicate installF20.install-duplicateMapped behavior
Scenario F20--duplicate-install · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extensions add”.
- DoEnter “$PWD/src/fixtures/canvas-extensions/demo-page” in “Add canvas extension source”.
- DoChoose “Add canvas extension submit”.
- WaitWait for “Add canvas extension submit” / “Install” to appear.
- DoPress Escape.
Expect
installing an app whose name is already installed is refused (409): the form stays open with its input, the toast says the app is already installed and points to Update or Uninstall on its card, and the existing card is unchanged.
Exact CLI recipe & bug checks
With the Git-installed demo present, run control-openhands browser errors --clear, control-openhands browser click 'testid=canvas-extensions-add-button', control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions/demo-page", control-openhands browser click 'testid=add-canvas-extension-submit' and control-openhands browser wait 'testid=add-canvas-extension-submit >> text=Install' --timeout 30000. control-openhands browser toasts reads This app is already installed. Use Update or Uninstall on its card. (browser screenshot --feature F20.install-duplicate --name already-installed), browser count 'testid=add-canvas-extension-modal' stays 1, browser count '[data-testid^="canvas-extension-card-"]' stays 1, api GET /api/canvas-extensions/installed still shows the GitHub source, and browser errors --app-only lists the expected 409 on POST /api/canvas-extensions/install. Close the form with control-openhands browser press Escape.
Busy lockF20.busy-lockMapped behavior
Scenario F20--busy-lock · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extension refresh demo page”.
- DoPress Space then --selector then testid=canvas-extension-card-demo-page >> role=switch.
- DoChoose “Confirmation dialog” / “Cancel”.
- DoUse browser focus as specified in the exact recipe.
- DoPress Space.
- DoReload the page.
Expect
while Update, Uninstall or Enable is pending, every card control is inert, the switch included.
Exact CLI recipe & bug checks
Needs the Git-installed demo from the Git tree URL bullet: its Update re-clones in about 0.4 to 2 s, so issue the next commands in the same shell line as the click. Disabled app: run control-openhands browser click 'testid=canvas-extension-refresh-demo-page', then at once control-openhands browser press Space --selector 'testid=canvas-extension-card-demo-page >> role=switch' (focuses the switch, then presses Space) and control-openhands browser eval "[document.querySelector('[data-testid=canvas-extension-uninstall-demo-page]').disabled, document.querySelector('[data-testid=canvas-extension-card-demo-page] [role=switch]').disabled, document.activeElement.tagName].join(' ')", which reads true true BODY (Uninstall and the switch are both disabled, and the busy switch takes no focus), then control-openhands browser count 'testid=confirmation-modal' (0: Space did nothing). Read the three values in that one eval: each browser enabled call takes about 150 ms and can land after a short Update. If the eval reads false for either control, the Update ended mid-line and Space may have reached an idle switch: close any trust dialog with control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button' and run the line again. Once control-openhands browser toasts --history shows App updated., the keyboard works again: browser enabled on the switch is true, and browser focus on it, browser press Space and browser count 'testid=confirmation-modal' give 1. Close the dialog with testid=confirmation-modal >> testid=cancel-button; api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is still false. Enabled app (only when the card's Ref equals the precondition's commit): enable it through the trust dialog and wait for sidebar-canvas-extension-demo-page-hello, then repeat the same line (click Update, Space on the switch, the eval reads true true BODY, no dialog). If the eval reads false for either control there and Space reached the idle switch, it turned the app off with no dialog (api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is false): enable it again through the trust dialog, wait for sidebar-canvas-extension-demo-page-hello and repeat the line. A few seconds later api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is still true, and after browser reload the switch's aria-checked is true and browser count 'testid=sidebar-canvas-extension-demo-page-hello' is 1. Clean up: uninstall as above and confirm api GET /api/canvas-extensions/installed --pick canvas_extensions is [].
Ref and Path with the `github:` shorthandF20.git-ref-pathMapped behavior
Scenario F20--ref-and-path-with-the-github-shorthand · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Canvas extensions add”.
- DoEnter “github:OpenHands/OpenHands” in “Add canvas extension source”.
- DoEnter “main” in “Add canvas extension ref”.
- DoEnter “src/fixtures/canvas-extensions/demo-page” in “Add canvas extension repo path”.
- DoUse browser focus as specified in the exact recipe.
- DoPress Enter.
- WaitWait for “Add canvas extension modal” to disappear.
- DoReload the page.
Expect
the Ref and Path fields apply to a Git source, including the placeholder's github:owner/repo shorthand; the card keeps the source as typed and shows the resolved commit and the path. Enter in a field submits the form.
Exact CLI recipe & bug checks
Needs the Git precondition. Run control-openhands browser click 'testid=canvas-extensions-add-button', control-openhands browser fill 'testid=add-canvas-extension-source-input' 'github:OpenHands/OpenHands', control-openhands browser fill 'testid=add-canvas-extension-ref-input' main, control-openhands browser fill 'testid=add-canvas-extension-repo-path-input' src/fixtures/canvas-extensions/demo-page, control-openhands browser focus 'testid=add-canvas-extension-repo-path-input', control-openhands browser press Enter (Enter submits) and control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 90000; the toast is App installed. …. After control-openhands browser reload, browser snapshot 'testid=canvas-extension-card-demo-page' shows source github:OpenHands/OpenHands, Ref with the precondition's commit and Path src/fixtures/canvas-extensions/demo-page. Uninstall it as above.
List error with RetryF20.load-errorBlocked
Scenario F20--list-error-with-retry · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupChange the run-owned service state: stop agent-server.
- DoOpen /apps.
Expect
when listing apps fails, the Installed section shows the error and a Retry button.
Exact CLI recipe & bug checks
Blocked: it needs an Agent Server whose GET /api/canvas-extensions/installed fails with a non-404 while its health checks pass. control-openhands service stop agent-server then control-openhands browser goto /apps shows the app-wide Manage backends gate (Disconnected) instead of the page, and on an already loaded page the cached list stays on screen.
Unsupported backendsF20.unsupportedBlocked
Scenario F20--unsupported-backends · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoLaunch an isolated stack.
Expect
on a Cloud backend, with no backend, or on an Agent Server without the apps API, the Installed section shows Not available with the reason and Add app is disabled; Cloud hides Apps from the Customize navigation.
Exact CLI recipe & bug checks
Blocked: needs a Cloud backend account (Apps are not available on Cloud backends yet., Apps hidden from Customize), a state with no backend (Add an Agent Server backend to use Apps.), or an Agent Server that passes Canvas's version gate (1.47.0 or newer) but has no /api/canvas-extensions routes (This Agent Server does not support Apps yet. Upgrade the backend and try again.). In each case Add app should be disabled. Released servers cannot produce the last state: the routes ship from 1.44.1, and an older server such as control-openhands launch --new --sdk-ref v1.43.1 (whose GET /api/canvas-extensions/installed is 404) never reaches /apps, because every page shows the Manage backends gate with Agent Canvas requires agent-server 1.47.0 or newer; this backend is running 1.43.1. (doctor still reports ok).
Malformed tree-URL escapesF20.git-tree-url · F20.install-errorKnown failure
Scenario F20--malformed-tree-url-escapes · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOn
/apps, opencanvas-extensions-add-button, filladd-canvas-extension-source-inputwithhttps://github.com/o/r/tree/main%/apps/demo, and clickadd-canvas-extension-submit. Inspectbrowser errors --app-only,browser toasts --history, and the modal count. Repeat withhttps://gitlab.com/group/repo/-/tree/main/apps/%E0%A4%A. Expected: submission produces a usable install/validation outcome. Known failure, reproduced 2026-10-12 onfa1f724with Agent Server 1.54.0: each submit raisesURI malformed, emits no toast, skips the install request, and leaves the modal open (#17783). A reviewed local demo source still installs successfully as the control; no malformed source is executed.
Expect
a Git host folder URL (…/<owner>/<repo>/tree/<ref>/<path>) pasted into App source is split into source, ref and path; the card shows the repo URL as source, Ref (resolved commit) and Path.
a failed install toasts the server's reason (Could not read canvas extension source: …) and keeps the form open with the input, for local paths and for unreachable Git sources alike.
Known bug observations
Known failure, reproduced 2026-10-12 on fa1f724 with Agent Server 1.54.0: each submit raises URI malformed, emits no toast, skips the install request, and leaves the modal open (#17783). A reviewed local demo source still installs successfully as the control; no malformed source is executed.
Exact CLI recipe & bug checks
On /apps, open canvas-extensions-add-button, fill add-canvas-extension-source-input with https://github.com/o/r/tree/main%/apps/demo, and click add-canvas-extension-submit. Inspect browser errors --app-only, browser toasts --history, and the modal count. Repeat with https://gitlab.com/group/repo/-/tree/main/apps/%E0%A4%A. Expected: submission produces a usable install/validation outcome. Known failure, reproduced 2026-10-12 on fa1f724 with Agent Server 1.54.0: each submit raises URI malformed, emits no toast, skips the install request, and leaves the modal open (#17783). A reviewed local demo source still installs successfully as the control; no malformed source is executed.
Extension page home pinF20.rail-entryKnown failure
Scenario F20--extension-page-home-pin · Permalink · Source
Given
Use F20’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoInstall and enable the reviewed local
src/fixtures/canvas-extensions/demo-pagethrough the form and trust confirmation. Hoversidebar-canvas-extension-demo-page-hello, inspectbrowser testids 'role=complementary' --filter pin, and compare with hoveringsidebar-automations-link. Expected: the enabled extension page offers Pin as home page and the normal home-pin lifecycle. Known failure, reproduced 2026-10-12 onfa1f724: only Automate exposessidebar-pin-home-toggle-automations; the extension page has no pin control (#16995).
Expect
an enabled app's page appears in the main sidebar rail as sidebar-canvas-extension-<name>-<pageId> linking to /extensions/<name>/<path>.
Known bug observations
Known failure, reproduced 2026-10-12 on fa1f724: only Automate exposes sidebar-pin-home-toggle-automations; the extension page has no pin control (#16995).
Exact CLI recipe & bug checks
Install and enable the reviewed local src/fixtures/canvas-extensions/demo-page through the form and trust confirmation. Hover sidebar-canvas-extension-demo-page-hello, inspect browser testids 'role=complementary' --filter pin, and compare with hovering sidebar-automations-link. Expected: the enabled extension page offers Pin as home page and the normal home-pin lifecycle. Known failure, reproduced 2026-10-12 on fa1f724: only Automate exposes sidebar-pin-home-toggle-automations; the extension page has no pin control (#16995).
Family notes, prerequisites for known issues & limits Known issue
- The card's refresh button (
canvas-extension-refresh-<name>) is labelled Update, not "Refresh". - The trust confirmation has no title and reuses the shared
confirmation-modal; its body repeats the amber notice. Space on the disabled app's Enable switch focuses Cancel. Tab and Shift+Tab wrap between Cancel and Enable trusted app; Escape and Cancel close it and restore focus to the still-disabled Enable switch. nav_labelis dropped by the Agent Server:GET /api/canvas-extensions/installedreturns pages with onlyid,titleandpath(Agent Server 1.50.1), so the rail shows the page title (OpenHands/software-agent-sdk#5501). KeepF20.rail-labelfailing until the backend returns it.- Install and update errors show the server's reason as sent, even when it says
Failed to fetch extension from <url>(an unreachable Git source). A response with no reason in it, such as the ingress's502while the Agent Server is stopped, shows the genericAn error occurred. Only a request that gets no response at all showsDisconnected (check URL or network)…; nocontrol-openhandsverb takes the ingress offline, so that state is not driven here. - The page states (empty, error, unsupported) and the
/extensions/...unavailable card have no test ids; assert on text withbrowser snapshot. - The Agent Server fetches Git sources with a shallow clone and appends
.gitto the repo URL: a test Git server must speak smart HTTP and answer bothqa-repoandqa-repo.git. - The Git bullets read the public upstream repo, so they depend on its
mainstill holding the identical fixture; the precondition'sdiff -rguards that. A Git install or Update clones in about 0.4 to 2 s here (Updates measured about 0.4 to 1.5 s on 2026-10-06), long enough for the busy-lock checks when they run in the same shell line as the click and read both controls in oneeval; on a slower network raise thewaittimeouts, not the expectations. - A duplicate install answers
409with the Agent Server's API hint (Canvas extension already installed. Use force=true to overwrite.); the form has no force option, so the toast replaces it with the Update/Uninstall advice. Plugins give the same advice since #18110 (F19 Gotchas). - Busy states with a local-path source last about 50 ms:
--observecatchesInstalling…, but checking that controls are disabled during Update needs a Git source (an upstream Update lasts about 0.4 to 2 s). control-openhands launchpasses the launcher only a fixed list of environment variables (path, locale, proxy) plus its own flags:OH_AGENT_SERVER_VERSION=<old>in the environment is ignored (the run still gets the pinned server); use--sdk-ref v<version>. Any server older than 1.47.0 is gated by Canvas before/appsrenders.- Stopping the Agent Server makes the next full page load show the Manage backends gate; always
restartanddoctorafterwards. - A trailing
browser errors --app-onlysweep shows a400per failed install by design; count only other errors. App backend view unavailableon the demo page is the expected text whileGET /server_infolists nocanvas_app_backend_bridge_v1capability (Agent Server 1.53.0); it does not mean the app failed to load. Cloud backends and the no-backend state never get a backend view either.- Known issue OpenHands/software-agent-sdk#5648 (
F20.app-backend-view): app-backend WebSocket subprotocols are reported dropped. Recheck on a Linux Agent Server with configured public ingress and reviewed platform-matching backend artifacts, using a genuine admitted app-backend session and subprotocol negotiation. SDK 1.55.0 supports only Linux app-backend artifacts, so a native macOS server cannot drive this branch. A default demo without the bridge capability cannot exercise the proxied WebSocket path. - Known issue OpenHands/software-agent-sdk#5935 (
F20.app-backend-view): an explicitOH_APP_BACKEND_PUBLIC_URL_IS_NONE=1does not clear a bridge URL set in the Agent Server JSON configuration. Canvas can therefore be offered an app-backend view at the unwanted configured ingress. Recheck an owned bridge-capable server with a JSON public URL and that environment-null override, then inspect/server_infoand the real app page; the default launcher has neither conflicting configuration source. - Known issue OpenHands/software-agent-sdk#5914 (
F20.app-backend-view): app-backend HTTP responses can carry both the backend's and the Agent Server'sDateandServerheaders, producing an invalid joined date and disclosing the backend stack. Recheck a reviewed HTTP app backend on a supported Linux Agent Server that sends these headers through a real admitted app-backend session. A default demo page without the bridge capability or a direct backend request cannot establish the proxied response.
Before you start
Use an OpenHands checkout at the source revision, with Node 24+, installed npm dependencies, uv / uvx, and a supported Chromium browser. The verification skill is the complete setup contract.
export PATH="$PWD/.agents/skills/verify-openhands/scripts:$PATH"
export OH_VERIFY_RUN=$(control-openhands launch --new --print-run)
control-openhands doctor
control-openhands onboard --skip
# Configure a model only for a scenario that needs one:
control-openhands llm preset deepseek
# When the run is complete:
control-openhands stop
F01 walks onboarding instead of skipping it. No-model checks run before model configuration. Keep OH_VERIFY_RUN set so commands address your own run. Desktop is 1440 × 1000; phone is 390 × 844. Model-backed checks need the configured provider key and budget.
Recipes run in source order within each family and can share fixtures. The page preserves that order; “as above” is a dependency on an earlier case. Setup commands and API writes arrange state. They do not prove a UI path. Follow the family cleanup steps.
A known failure preserves the intended expectation beside the observed defect. Executable known-bug blocks use xfail for a reproduced symptom and xpass when it stops reproducing. Cloud accounts, enterprise capabilities and other unavailable prerequisites remain explicit. “Not run” is different from “blocked”.
The maintained map describes application behavior at its pinned revision. Once a fix is verified in the application version under test, remove the obsolete failure note and keep the normal expectation. Dated evidence belongs in the backfill ledger.
Read the full map index and coverage boundaries, the CLI guide, or the earlier illustrated edition. The verification approach adapts Lauren Tan’s (@poteto) pstack verification-skill generators; attribution and adaptation.