Plugins and plugin launch
F19 · 25 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.
F19 Plugins and plugin launch
25 behaviors · 34 scripts & contracts · Maintained source
Entry points & prerequisites
What this family covers
Plugins bundle skills, commands and files that the agent loads into a conversation. Under Customize → Plugins a user browses one card grid that merges the marketplace catalog, installed plugins and read-only local plugins; searches and filters it; installs, enables, disables, updates and uninstalls plugins; browses a plugin's bundled skills and files; and starts a conversation with a plugin. Starting goes through /launch, a review screen (also reachable as a deep link) that lists the plugins, lets the user edit plugin parameters and requires a trust checkbox before it creates the conversation.
How a user gets here
- Sidebar Customize (
sidebar-skills-link, lands on/mcp), then Plugins in the Customize navigation (sidebar-extensions-/plugins). - Direct URL
/plugins. - Command menu (
Control+k/Meta+k): "Plugins" finds only Customize ("Browse skills, plugins, and integrations"), which lands on/mcp; then click Plugins. There is no direct Plugins command. - Phone width:
/customizeshows the Customize hub; its Plugins row (extensions-mobile-hub >> sidebar-extensions-/plugins) opens/plugins, and the top bar's Back chevron (sidebar-mobile-back-button) returns to/customize. /launch: from a plugin's detail modal (Start Conversation), or a deep link/launch?plugins=<base64 JSON array>&message=<text>(production format) or/launch?plugin_source=…(dev format).- Attaching plugins from the home composer (Plugins pill) is F03 (
F03.plugin-picker); the conversation's Show Plugins list is F27 (F27.plugins-modal).
Given
Preconditions:
- Baseline state (launched, doctored,
onboard --skipdone), local backend, desktop viewport. - An active LLM profile (
control-openhands llm preset deepseek) for Enabled plugins auto-load and the two Start-a-conversation bullets. - No plugin installed:
control-openhands api GET /api/plugins/installedreturns{"plugins": []}. - The catalog comes from the launcher's cached public-skills checkout and needs no network:
control-openhands api GET /api/plugins/marketplacelistscity-weather,magic-testand others with local-pathsourcevalues.magic-testanswers "alakazam" withThe magic worked! Plugin loaded successfully. 🎩✨, which makes plugin loading cheap to prove. - Right before the Local plugin bullet (not earlier: it changes card counts and the Local filter), put a local plugin in the Agent Server's
~/.agents/plugins. No CLI verb does this (harness gap); from the checkout runH="$OH_VERIFY_RUN/private/home/.agents/plugins"; mkdir -p "$H" && cp -r "$OH_VERIFY_RUN/private/cache/skills/public-skills/plugins/magic-test" "$H/qa-local-plugin" && sed -i 's/"name": "magic-test"/"name": "qa-local-plugin"/' "$H/qa-local-plugin/.plugin/plugin.json". The rename matters: a local plugin whose name is already in the catalog is hidden. - The Add plugin bullet installs from
github:OpenHands/extensions, which needs outbound access to GitHub.
Open the pageF19.pageMapped behavior
Scenario F19--open-the-page · Permalink · Source
Given
Use F19’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 /conversations.
- DoChoose “Sidebar skills”.
- DoChoose “Sidebar extensions /plugins”.
- WaitWait for the element identified in the CLI recipe to appear.
Expect
/plugins shows the Plugins heading, the description "Browse, install, enable, and uninstall plugins. Enabled plugins load automatically into new conversations.", an enabled Add plugin button on a local backend, and the catalog as cards.
Exact CLI recipe & bug checks
Run control-openhands browser goto /conversations, control-openhands browser click 'testid=sidebar-skills-link' --expect-url '/mcp(\?|$)', control-openhands browser click 'testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)', then control-openhands browser text 'testid=skills-plugins-screen >> h2' (Plugins), control-openhands browser text 'testid=plugins-settings-description' (the description above), control-openhands browser enabled 'testid=plugins-add-plugin-button' (true), control-openhands browser wait '[data-testid^="plugin-card-"]' --timeout 60000 (the catalog loads after the page: an immediate count can read 0) and control-openhands browser count '[data-testid^="plugin-card-"]' (9 with today's catalog). control-openhands browser screenshot --feature F19.page --name catalog shows two columns of cards, each with name, truncated source path, a two-line description and Install.
SearchF19.searchMapped behavior
Scenario F19--search · Permalink · Source
Given
Use F19’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 “weather” in “Plugins search”.
- DoEnter “alakazam” in “Plugins search”.
- DoEnter “zzz-none” in “Plugins search”.
- DoChoose “Plugins toolbar” / “Clear search”.
Expect
the search box filters by name, description, source, path and ref; a query matching nothing shows No plugins match your search.; the X clears the query.
Exact CLI recipe & bug checks
Run control-openhands browser fill 'testid=plugins-search-input' weather and control-openhands browser count '[data-testid^="plugin-card-"]' (1). control-openhands browser fill 'testid=plugins-search-input' alakazam also leaves 1 (magic-test, matched on its description). control-openhands browser fill 'testid=plugins-search-input' zzz-none, then control-openhands browser text 'testid=plugins-no-match': No plugins match your search. Run control-openhands browser click 'testid=plugins-toolbar >> role=button[name="Clear search"]'; control-openhands browser value 'testid=plugins-search-input' is empty and the count is back to 9.
Status filterF19.status-filterMapped behavior
Scenario F19--status-filter · Permalink · Source
Given
Use F19’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 “Plugins filter installed”.
- DoChoose “Plugins filter available”.
- DoChoose “Plugins filter local”.
- DoReload the page.
Expect
the All / Installed / Available / Local segments filter the grid; the active one has aria-pressed="true"; the filter resets to All on reload.
Exact CLI recipe & bug checks
control-openhands browser attr 'testid=plugins-filter-all' aria-pressed is true. Run control-openhands browser click 'testid=plugins-filter-installed': its aria-pressed is true, All's is false, and with nothing installed control-openhands browser count 'testid=plugins-no-match' is 1. control-openhands browser click 'testid=plugins-filter-available' shows all 9 cards; control-openhands browser click 'testid=plugins-filter-local' shows plugins-no-match (no local plugin yet). After control-openhands browser reload, plugins-filter-all is pressed again.
Detail modalF19.detail-modalMapped behavior
Scenario F19--detail-modal · Permalink · Source
Given
Use F19’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 “Plugin card magic test”.
- DoChoose “Plugin detail modal close”.
- DoUse browser focus as specified in the exact recipe.
- DoPress Enter.
- DoPress Escape.
- DoUse browser focus as specified in the exact recipe.
- DoPress Space.
- DoChoose “Plugin detail modal dismiss”.
Expect
clicking a card (or Enter/Space on a focused card) opens a modal with name, source, description, Skills in this plugin bundle, Files and footer actions; X, Close and Escape close it.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=plugin-card-magic-test' and control-openhands browser snapshot 'testid=plugin-detail-modal': heading magic-test, the source path, the description, Skills in this plugin bundle with magic-word, Files with .plugin and skills folders, and buttons Install, Start Conversation, Close. Close with control-openhands browser click 'testid=plugin-detail-modal-close' (browser count 'testid=plugin-detail-modal' is 0). Keyboard: control-openhands browser focus 'testid=plugin-card-city-weather', control-openhands browser press Enter, then control-openhands browser attr 'testid=plugin-detail-modal' data-plugin-name (city-weather); control-openhands browser press Escape closes it; browser focus again and control-openhands browser press Space reopens it; control-openhands browser click 'testid=plugin-detail-modal-dismiss' closes it.
FilesF19.files-browserMapped behavior
Scenario F19--files · Permalink · Source
Given
Use F19’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 “File tree dir .plugin”.
- DoChoose “File tree file .plugin/plugin.json”.
- WaitWait for “Plugin file content” / “magic-test” to appear.
Expect
in the modal's Files tree, expanding a folder and selecting a file shows its highlighted content with line numbers; selecting it again closes the viewer.
Exact CLI recipe & bug checks
Open the magic-test modal again and run control-openhands browser click 'testid=file-tree-dir-.plugin', control-openhands browser click 'testid=file-tree-file-.plugin/plugin.json', control-openhands browser wait 'testid=plugin-file-content >> text=magic-test' --timeout 15000 and control-openhands browser text 'testid=plugin-file-content': .plugin/plugin.json followed by numbered JSON lines (2 "name": "magic-test",). control-openhands browser screenshot --feature F19.files-browser --name plugin-json shows the tree with the file highlighted above a syntax-coloured viewer. Click the same file again; control-openhands browser count 'testid=plugin-file-content' is 0. Close with testid=plugin-detail-modal-close.
Install from a cardF19.install-cardMapped behavior
Scenario F19--install-from-a-card · Permalink · Source
Given
Use F19’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 “Plugin install magic test”.
- WaitWait for “Plugin toggle magic test” to appear.
- DoReload the page.
- DoChoose “Plugins filter installed”.
Expect
Install on a card installs the plugin: toast Plugin installed., the button becomes an enable switch, a version pill appears, the card sorts first and the install survives a reload.
Exact CLI recipe & bug checks
Run control-openhands browser toasts --clear, control-openhands browser click 'testid=plugin-install-magic-test', control-openhands browser wait 'testid=plugin-toggle-magic-test' --timeout 60000 and control-openhands browser toasts (Plugin installed.). control-openhands browser text 'testid=plugin-version-magic-test' is v1.0.0 and control-openhands browser eval "[...document.querySelectorAll('[data-testid^=plugin-card-]')].map(e=>e.dataset.testid)[0]" is plugin-card-magic-test. After control-openhands browser reload and control-openhands browser click 'testid=plugins-filter-installed', browser count '[data-testid^="plugin-card-"]' is 1, and control-openhands api GET /api/plugins/installed lists magic-test with "enabled": true. Click testid=plugins-filter-all.
Enable switchF19.enable-toggleKnown issue
Scenario F19--enable-switch · Permalink · Source
Given
Use F19’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
- DoHover over “Plugin toggle magic test”.
- DoChoose “Plugin toggle magic test”.
- DoReload the page.
- DoChoose “Plugin card magic test”.
- DoChoose “Plugin detail modal” / “"Disabled"”.
Expect
the card switch (named Disable plugin / Enable plugin) and the modal Enabled/Disabled switch turn an installed plugin off and on; the state persists after reload.
Linked known issues
Known issue OpenHands/software-agent-sdk#5985: Update can re-enable a disabled plugin when an installed-list request overlaps the force-reinstall directory swap. A live check needs an owned disabled plugin and a real concurrent list that lands in that window; the report's monkeypatched copytree schedule is not Canvas live proof. No overlap has been proved in this pass. (F19.refresh, F19.enable-toggle).
Exact CLI recipe & bug checks
Run control-openhands browser attr 'testid=plugin-toggle-magic-test' aria-checked (true) and control-openhands browser attr 'testid=plugin-toggle-magic-test' aria-label (Disable plugin), control-openhands browser hover 'testid=plugin-toggle-magic-test', control-openhands browser click 'testid=plugin-toggle-magic-test', wait three seconds, then browser attr ... aria-checked again (false, with aria-label Enable plugin); after control-openhands browser reload it is still false and api GET /api/plugins/installed shows "enabled": false. Today the disable is often lost: the reload's concurrent reads hit the metadata race (Gotchas) and the API, then the switch, read "enabled": true again (2 of 3 trials; the PATCH itself returns 200). If that happens, disable it once more before the modal step, which needs the plugin disabled. In the modal: control-openhands browser click 'testid=plugin-card-magic-test', control-openhands browser click 'testid=plugin-detail-modal >> text="Disabled"', then control-openhands browser eval "document.querySelector('[data-testid=plugin-modal-toggle-magic-test]').checked" (true); the API shows "enabled": true and, after closing the modal, the card switch's aria-checked is true.
Enabled plugins auto-loadF19.enabled-autoloadMapped behavior
Scenario F19--enabled-plugins-auto-load · Permalink · Source
Given
Use F19’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
- DoStart a conversation with the prompt “alakazam”.
- DoOpen /plugins.
- DoStart a conversation as specified in the recipe.
- DoOpen /plugins.
Expect
an installed, enabled plugin loads into every new conversation; disabled, it does not.
Exact CLI recipe & bug checks
With magic-test enabled, run control-openhands conversation start --prompt "alakazam" --wait --timeout 240 and control-openhands browser text 'testid=agent-message >> nth=-1': The magic worked! Plugin loaded successfully. 🎩✨. Then control-openhands browser goto /plugins, disable it with the card switch (hover, click; API "enabled": false) and run the same conversation start: the reply is an ordinary request for a task, without the phrase. browser goto /plugins again; magic-test stays installed and disabled. Today the revisit often re-enables it (API "enabled": true, installed_at reset; see Gotchas), so re-check the API before relying on the disabled state.
UpdateF19.refreshKnown failureKnown issue
Scenario F19--update · Permalink · Source
Given
Use F19’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 “Plugin card magic test”.
- DoChoose “Plugin detail refresh magic test”.
Expect
Update in an installed plugin's modal re-fetches it from its recorded source and shows Plugin updated.
Known bug observations
Known failure (reproduced 2026-10-09): a page revisit after a catalog install usually rewrites the source to local (OpenHands/software-agent-sdk#5496, see Gotchas), and Update then answers 500 with one toast carrying the server's reason, Unable to parse extension source: local. Expected formats: 'github:owner/repo', git URL, or local path (browser screenshot --feature F19.refresh --name local-source-error). On a plugin whose source survived (the GitHub install in Add plugin from GitHub, browser text 'testid=plugin-source-magic-test' = github:OpenHands/extensions), the same click logs Updating extension magic-test from github:OpenHands/extensions and toasts --history shows Plugin updated. Close the modal.
Linked known issues
Known issue OpenHands/software-agent-sdk#5985: Update can re-enable a disabled plugin when an installed-list request overlaps the force-reinstall directory swap. A live check needs an owned disabled plugin and a real concurrent list that lands in that window; the report's monkeypatched copytree schedule is not Canvas live proof. No overlap has been proved in this pass. (F19.refresh, F19.enable-toggle).
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=plugin-card-magic-test', control-openhands browser toasts --clear, control-openhands browser click 'testid=plugin-detail-refresh-magic-test', wait a few seconds, then control-openhands browser toasts. Expected Plugin updated. (the toast fades fast: read control-openhands browser toasts --history). Whether it works depends on the recorded source (control-openhands browser text 'testid=plugin-source-magic-test'): with the catalog path it does. Known failure (reproduced 2026-10-09): a page revisit after a catalog install usually rewrites the source to local (OpenHands/software-agent-sdk#5496, see Gotchas), and Update then answers 500 with one toast carrying the server's reason, Unable to parse extension source: local. Expected formats: 'github:owner/repo', git URL, or local path (browser screenshot --feature F19.refresh --name local-source-error). On a plugin whose source survived (the GitHub install in Add plugin from GitHub, browser text 'testid=plugin-source-magic-test' = github:OpenHands/extensions), the same click logs Updating extension magic-test from github:OpenHands/extensions and toasts --history shows Plugin updated. Close the modal.
Start a conversation from an installed pluginF19.start-conversationMapped behavior
Scenario F19--start-a-conversation-from-an-installed-plugin · Permalink · Source
Given
Use F19’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 “Plugin card magic test”.
- DoChoose “Plugin detail start conversation magic test”.
- DoUse browser check as specified in the exact recipe.
- DoChoose “Start conversation”.
- DoEnter “alakazam” in “Message”.
- DoPress Enter then --selector then testid=chat-input.
Expect
Start Conversation in the detail modal opens /launch?plugins=<base64 JSON> for that plugin.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=plugin-card-magic-test', control-openhands browser click 'testid=plugin-detail-start-conversation-magic-test' --expect-url '/launch\?plugins=' and control-openhands browser eval "JSON.parse(atob(new URLSearchParams(location.search).get('plugins')))". Expected: the plugin's real source. Today it is [{"source":"local","ref":null,"repo_path":null}] and the modal title is Launch local; after control-openhands browser check 'testid=trust-checkbox' and control-openhands browser click 'testid=start-conversation-button' --expect-url '/conversations/[0-9a-f-]+', control-openhands browser type 'testid=chat-input' 'alakazam' and control-openhands browser press Enter --selector 'testid=chat-input' leave the message at Sending... (still after 25 s) with the banner Unable to parse plugin source: local. Expected formats: 'github:owner/repo', git URL, or local path (control-openhands browser screenshot --feature F19.start-conversation --name local-source-conversation). When the installed plugin kept its source (the GitHub install below), the link carries it: [{"source":"github:OpenHands/extensions","ref":"<resolved commit>","repo_path":"plugins/magic-test"}] and the title is Launch magic-test.
UninstallF19.uninstallMapped behavior
Scenario F19--uninstall · Permalink · Source
Given
Use F19’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 /plugins.
- DoChoose “Plugin card magic test”.
- DoChoose “Plugin detail uninstall magic test”.
- DoReload the page.
Expect
Uninstall removes the plugin at once (no confirmation), shows Plugin uninstalled., closes the modal and returns the card to Install.
Exact CLI recipe & bug checks
Run control-openhands browser goto /plugins, control-openhands browser toasts --clear, control-openhands browser click 'testid=plugin-card-magic-test', control-openhands browser click 'testid=plugin-detail-uninstall-magic-test', then control-openhands browser count 'testid=plugin-detail-modal' (0), control-openhands browser count 'testid=confirmation-modal' (0: no confirmation), control-openhands browser toasts (Plugin uninstalled.) and control-openhands browser count 'testid=plugin-install-magic-test' (1). After control-openhands browser reload the Install button is still there, browser text 'testid=plugin-source-magic-test' is the catalog path again and api GET /api/plugins/installed returns {"plugins": []}.
Install from the modalF19.install-modalMapped behavior
Scenario F19--install-from-the-modal · Permalink · Source
Given
Use F19’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 “Plugin card city weather”.
- DoChoose “Plugin detail install city weather”.
- WaitWait for “Plugin detail uninstall city weather” to appear.
- DoChoose “Plugin detail uninstall city weather”.
Expect
Install in the detail modal does the same for the open plugin; the modal stays open and switches to the installed footer.
Exact CLI recipe & bug checks
Run control-openhands browser toasts --clear, control-openhands browser click 'testid=plugin-card-city-weather', control-openhands browser click 'testid=plugin-detail-install-city-weather', control-openhands browser wait 'testid=plugin-detail-uninstall-city-weather' --timeout 60000 and control-openhands browser toasts (Plugin installed.). The modal stays open (browser count 'testid=plugin-detail-modal' is 1), control-openhands browser eval "document.querySelector('[data-testid=plugin-modal-toggle-city-weather]').checked" is true, and control-openhands browser snapshot 'testid=plugin-detail-modal >> footer' lists Update, Uninstall, Start Conversation, Close. Clean up with control-openhands browser click 'testid=plugin-detail-uninstall-city-weather'; browser count 'testid=plugin-install-city-weather' is 1.
Cancel Add pluginF19.add-plugin-cancelMapped behavior
Scenario F19--cancel-add-plugin · Permalink · Source
Given
Use F19’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 “Plugins add plugin”.
- DoEnter “qa-typed” in “Add plugin source”.
- DoChoose “Add plugin modal close”.
- DoChoose “Add plugin modal dismiss”.
- DoPress Escape.
- DoUse browser mouse click as specified in the exact recipe.
Expect
the Add modal's X, Close, Escape and a backdrop click close it without installing; reopening starts with empty fields.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=plugins-add-plugin-button', control-openhands browser fill 'testid=add-plugin-source-input' qa-typed and control-openhands browser click 'testid=add-plugin-modal-close'; control-openhands browser count 'testid=add-plugin-modal' is 0. Reopen it: control-openhands browser value 'testid=add-plugin-source-input' is empty. Type again and close with control-openhands browser click 'testid=add-plugin-modal-dismiss' (count 0); reopen and control-openhands browser press Escape (count 0); reopen and control-openhands browser mouse-click 20 500 on the backdrop (count 0). api GET /api/plugins/installed still returns {"plugins": []} and browser network shows no POST /api/plugins/install.
Add plugin form and errorF19.add-plugin · F19.add-plugin-errorMapped behavior
Scenario F19--add-plugin-form-and-error · Permalink · Source
Given
Use F19’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 “Plugins add plugin”.
- DoEnter “ ” in “Add plugin source”.
- DoEnter “qa-not-a-source” in “Add plugin source”.
- DoChoose “Add plugin submit”.
Expect
Add plugin opens Add a plugin with Source (required), Ref and Path (optional); Install stays disabled until Source has text; a valid source installs and closes the modal.
an unusable source shows one error toast with the server's reason and keeps the modal open with the typed values; a plugin that is already installed (409) toasts This plugin is already installed. Open its card to Update or Uninstall it. instead (#18110).
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=plugins-add-plugin-button' and control-openhands browser snapshot 'testid=add-plugin-modal': heading Add a plugin, Install a plugin from a Git source or local path., textbox Source (placeholder github:owner/repo, a Git URL, or a local path), Ref Optional, Path Optional, Close and a disabled Install. control-openhands browser fill 'testid=add-plugin-source-input' ' ' keeps control-openhands browser enabled 'testid=add-plugin-submit' at false; control-openhands browser fill 'testid=add-plugin-source-input' qa-not-a-source turns it true. Run control-openhands browser toasts --clear, control-openhands browser click 'testid=add-plugin-submit', wait a few seconds, then control-openhands browser toasts --history, control-openhands browser count 'testid=add-plugin-modal' (1) and control-openhands browser value 'testid=add-plugin-source-input' (qa-not-a-source). The history holds one toast with the server's reason: the Agent Server answers 400 Failed to fetch plugin source. Check that the source is valid., and the toast shows that text (browser screenshot --feature F19.add-plugin-error --name toast).
Add plugin from GitHubF19.add-pluginMapped behavior
Scenario F19--add-plugin-from-github · Permalink · Source
Given
Use F19’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 “github:OpenHands/extensions” in “Add plugin source”.
- DoEnter “plugins/magic-test” in “Add plugin repo path”.
- DoChoose “Add plugin submit”.
- WaitWait for “Add plugin modal” to disappear.
Expect
Add plugin opens Add a plugin with Source (required), Ref and Path (optional); Install stays disabled until Source has text; a valid source installs and closes the modal.
Exact CLI recipe & bug checks
In the same modal run control-openhands browser fill 'testid=add-plugin-source-input' 'github:OpenHands/extensions', control-openhands browser fill 'testid=add-plugin-repo-path-input' 'plugins/magic-test', control-openhands browser toasts --clear, control-openhands browser click 'testid=add-plugin-submit' and control-openhands browser text 'testid=add-plugin-submit' (Installing…), then control-openhands browser wait 'testid=add-plugin-modal' --state hidden --timeout 120000 and control-openhands browser toasts (Plugin installed.). api GET /api/plugins/installed lists magic-test with four files (.claude-plugin/plugin.json, .codex-plugin/plugin.json, .plugin/plugin.json, skills/magic-word/SKILL.md) with "source": "github:OpenHands/extensions", "repo_path": "plugins/magic-test" and a commit in resolved_ref; the card's plugin-source-magic-test reads github:OpenHands/extensions. The metadata race (Gotchas) can still turn it into "source": "local" with repo_path: null. Without GitHub access expect the error toast of the previous bullet instead. Clean up: testid=plugin-card-magic-test → testid=plugin-detail-uninstall-magic-test.
Installed coordinatesF19.installed-coordinatesMapped behavior
Scenario F19--installed-coordinates · Permalink · Source
Given
Use F19’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 “Plugins add plugin”.
- DoEnter “github:OpenHands/extensions” in “Add plugin source”.
- DoEnter “main” in “Add plugin ref”.
- DoEnter “plugins/city-weather” in “Add plugin repo path”.
- DoChoose “Add plugin submit”.
- WaitWait for “Add plugin modal” to disappear.
- DoChoose “Plugin card city weather”.
- DoChoose “Plugin detail uninstall city weather”.
Expect
a plugin installed with a source, ref and path shows that source on its card and in the modal header, and modal pills v<version>, Ref <resolved commit> and Path <repo path> next to the Enabled switch.
Exact CLI recipe & bug checks
Needs GitHub access like the previous bullet. Run control-openhands browser click 'testid=plugins-add-plugin-button', control-openhands browser fill 'testid=add-plugin-source-input' 'github:OpenHands/extensions', control-openhands browser fill 'testid=add-plugin-ref-input' main, control-openhands browser fill 'testid=add-plugin-repo-path-input' 'plugins/city-weather', control-openhands browser click 'testid=add-plugin-submit' and control-openhands browser wait 'testid=add-plugin-modal' --state hidden --timeout 120000 (toasts --history: Plugin installed.). control-openhands browser text 'testid=plugin-source-city-weather' is github:OpenHands/extensions; control-openhands browser click 'testid=plugin-card-city-weather' and control-openhands browser snapshot 'testid=plugin-detail-modal' show the header paragraph github:OpenHands/extensions and the text v1.0.0 Ref <40-hex commit> Path plugins/city-weather Enabled (the API's resolved_ref), and Files lists .claude-plugin and .codex-plugin that the catalog copy lacks. Clean up with control-openhands browser click 'testid=plugin-detail-uninstall-city-weather'; the card's source is the catalog path again.
Local pluginF19.local-pluginMapped behavior
Scenario F19--local-plugin · Permalink · Source
Given
Use F19’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
- DoReload the page.
- DoChoose “Plugins filter local”.
- DoChoose “Plugin card qa local plugin”.
- DoReload the page.
Expect
a plugin found in ~/.agents/plugins shows a Local badge (no Install or switch), is listed under the Local filter, and its modal has a Local pill, Files, no Install/Uninstall and no Start Conversation.
Exact CLI recipe & bug checks
After arranging qa-local-plugin (Preconditions), run control-openhands browser reload, control-openhands browser text 'testid=plugin-local-badge-qa-local-plugin' (Local), and control-openhands browser count on testid=plugin-install-qa-local-plugin, testid=plugin-toggle-qa-local-plugin and testid=plugin-source-qa-local-plugin (all 0). control-openhands browser click 'testid=plugins-filter-local' leaves one card; under plugins-filter-available browser count 'testid=plugin-card-qa-local-plugin' is 0. Back on All, control-openhands browser click 'testid=plugin-card-qa-local-plugin' and control-openhands browser snapshot 'testid=plugin-detail-modal': no source line, pills Local v1.0.0, the bundled skill and Files, and only a Close button in the footer (browser screenshot --feature F19.local-plugin --name modal). Close with Escape. Clean up: rm -rf "$OH_VERIFY_RUN/private/home/.agents/plugins/qa-local-plugin", control-openhands browser reload; the card count for qa-local-plugin is 0.
Start a conversation from the catalogF19.start-conversation · F19.launch-trustMapped behavior
Scenario F19--start-a-conversation-from-the-catalog · Permalink · Source
Given
Use F19’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 “Plugin card magic test”.
- DoChoose “Plugin detail start conversation magic test”.
- DoUse browser check as specified in the exact recipe.
- DoChoose “Start conversation”.
- DoChoose “Chat plus”.
- DoChoose “Show plugins”.
- DoEnter “alakazam” in “Message”.
- DoPress Enter then --selector then testid=chat-input.
- WaitWait for the conversation to finish.
Expect
Start Conversation in the detail modal opens /launch?plugins=<base64 JSON> for that plugin.
Start Conversation stays disabled until the trust checkbox (I trust this plugin from <sources> …) is ticked; then it creates the conversation (sending the message, if any), opens /conversations/<id>, and the plugin is loaded there.
Exact CLI recipe & bug checks
With magic-test not installed, run control-openhands browser click 'testid=plugin-card-magic-test', control-openhands browser click 'testid=plugin-detail-start-conversation-magic-test' --expect-url '/launch\?plugins=' and the atob eval above: one object with the catalog source, ref: null, repo_path: null. control-openhands browser snapshot 'testid=plugin-launch-modal' shows Launch magic-test, the source path and the checkbox I trust this plugin from <source path> with the agent secrets defined in my account.; control-openhands browser enabled 'testid=start-conversation-button' is false. Run control-openhands browser check 'testid=trust-checkbox' (enabled turns true) and control-openhands browser click 'testid=start-conversation-button' --expect-url '/conversations/[0-9a-f-]+'; control-openhands browser url gives <id>. control-openhands browser click 'testid=chat-plus-button', control-openhands browser click 'testid=show-plugins-button' and control-openhands browser text 'testid=plugins-modal' list magic-test; close with testid=close-plugins-modal. Then control-openhands browser type 'testid=chat-input' 'alakazam', control-openhands browser press Enter --selector 'testid=chat-input', control-openhands conversation wait <id> --timeout 180 and control-openhands browser text 'testid=agent-message >> nth=-1': the magic phrase.
Review screenF19.launch-reviewMapped behavior
Scenario F19--review-screen · Permalink · Source
Given
Use F19’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 /launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIifSx7InNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9PcGVuSGFuZHMvZXh0ZW5zaW9ucy5naXQiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL21hZ2ljLXRlc3QiLCJyZWYiOiJtYWluIn1d.
- DoOpen /launch?plugin_source=github%3AOpenHands%2Fextensions&message=%3Cb%3EWeather%3C%2Fb%3E%20for%20QA.
- DoChoose “Plugin launch modal” / “Close”.
- DoPress Escape.
Expect
/launch shows a modal titled Launch <plugin> (one plugin) or Launch Plugin (several), the optional message with HTML stripped and capped at 500 characters, and each plugin as <source> / <path> @ <ref>; X and Escape go to /conversations.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIifSx7InNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9PcGVuSGFuZHMvZXh0ZW5zaW9ucy5naXQiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL21hZ2ljLXRlc3QiLCJyZWYiOiJtYWluIn1d' (two plugins from OpenHands/extensions, one as an HTTPS git URL) and control-openhands browser snapshot 'testid=plugin-launch-modal': the heading Launch Plugin (Launch once); under Plugins, the text city-weather OpenHands/extensions/ plugins/city-weather magic-test OpenHands/extensions/ plugins/magic-test@ main (the screenshot renders OpenHands/extensions / plugins/magic-test @ main); and the checkbox I trust this plugin from OpenHands/extensions with the agent secrets defined in my account., which names OpenHands/extensions once (browser screenshot --feature F19.launch-review --name multi). Message: control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&message=%3Cb%3EWeather%3C%2Fb%3E%20for%20QA' and control-openhands browser text 'testid=plugin-launch-modal >> p': Weather for QA (tags stripped). With a 600-character message (/launch?plugin_source=github%3AOpenHands%2Fextensions&message=aaa…), control-openhands browser eval "document.querySelector('[data-testid=plugin-launch-modal] p').textContent.length" is 500. Close: control-openhands browser click 'testid=plugin-launch-modal >> testid=close-button' --expect-url '/conversations(\?|$)'; on a fresh /launch?..., control-openhands browser press Escape also lands on /conversations.
ParametersF19.launch-parametersMapped behavior
Scenario F19--parameters · Permalink · Source
Given
Use F19’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 /launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIiLCJyZWYiOiJtYWluIiwicGFyYW1ldGVycyI6eyJjaXR5IjoiUGFyaXMiLCJkYXlzIjozLCJtZXRyaWMiOnRydWV9fV0%3D.
- DoChoose “Plugin section 0”.
- DoEnter “Berlin” in “Plugin 0 param city”.
- DoEnter “” in “Plugin 0 param days”.
- DoUse browser uncheck as specified in the exact recipe.
Expect
plugins with parameters get an expanded, collapsible section with a text input, number input (blank becomes 0) or checkbox per value; edits are kept.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIiLCJyZWYiOiJtYWluIiwicGFyYW1ldGVycyI6eyJjaXR5IjoiUGFyaXMiLCJkYXlzIjozLCJtZXRyaWMiOnRydWV9fV0%3D' (city-weather with {"city":"Paris","days":3,"metric":true}) and control-openhands browser snapshot 'testid=plugin-launch-modal': a city-weather section button, ref: main, path: plugins/city-weather, textbox city (Paris), spinbutton days (3) and a checked checkbox metric. control-openhands browser click 'testid=plugin-section-0' collapses it (browser count 'testid=plugin-0-param-city' is 0); click again to expand. control-openhands browser fill 'testid=plugin-0-param-city' Berlin, control-openhands browser fill 'testid=plugin-0-param-days' '' (then control-openhands browser value 'testid=plugin-0-param-days' is 0) and control-openhands browser uncheck 'testid=plugin-0-param-metric' are accepted. A link that mixes plugins with and without parameters (/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9tYWdpYy10ZXN0In0seyJzb3VyY2UiOiJnaXRodWI6T3BlbkhhbmRzL2V4dGVuc2lvbnMiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL2NpdHktd2VhdGhlciIsInBhcmFtZXRlcnMiOnsiY2l0eSI6IlBhcmlzIn19XQ%3D%3D) lists the parameter-less one under Additional Plugins. Leave with Escape; do not start this one (see Gotchas).
Dev formatF19.launch-dev-paramsMapped behavior
Scenario F19--dev-format · Permalink · Source
Given
Use F19’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 /launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_ref=main&plugin_repo_path=plugins%2Fmagic-test&message=hi%20QA.
Expect
/launch?plugin_source=…&plugin_ref=…&plugin_repo_path=…&message=… opens the same review for one plugin.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_ref=main&plugin_repo_path=plugins%2Fmagic-test&message=hi%20QA' and control-openhands browser snapshot 'testid=plugin-launch-modal': Launch magic-test, paragraph hi QA, text magic-test OpenHands/extensions/ plugins/magic-test@ main.
Error statesF19.launch-errorsMapped behavior
Scenario F19--error-states · Permalink · Source
Given
Use F19’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 <url>.
- DoChoose “Go home”.
Expect
bare /launch or an empty array says No plugins were specified…; bad base64, invalid JSON, a non-array or an item without source says The plugin configuration is invalid…; Go Home opens /conversations.
Exact CLI recipe & bug checks
For each of /launch, /launch?plugins=garbage, /launch?plugins=e30%3D ({}), /launch?plugins=W10%3D ([]) and /launch?plugins=W3sicmVmIjoibWFpbiJ9XQ%3D%3D ([{"ref":"main"}]) run control-openhands browser goto '<url>' and control-openhands browser text 'testid=launch-error'. Bare and [] read Unable to Launch / No plugins were specified. Please provide at least one plugin to launch.; the other three read The plugin configuration is invalid. Please check the URL and try again. (browser screenshot --feature F19.launch-errors --name invalid-format). control-openhands browser count 'testid=try-again-button' is 0 in all of them. control-openhands browser click 'testid=go-home-button' --expect-url '/conversations(\?|$)' leaves.
Creation failureF19.launch-creation-failedMapped behavior
Scenario F19--creation-failure · Permalink · Source
Given
Use F19’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 /launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_repo_path=plugins%2Fmagic-test.
- DoUse browser check as specified in the exact recipe.
- SetupChange the run-owned service state: stop agent-server.
- DoChoose “Start conversation”.
- WaitWait for “Launch error” to appear.
- DoChoose “Try again”.
- DoRestart the run-owned stack as specified.
- DoReload the page.
Expect
when conversation creation fails, the screen shows Unable to Launch / Failed to Start Conversation with Go Home and Try Again; Try Again reloads the page, which brings back the review modal with the trust box cleared.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_repo_path=plugins%2Fmagic-test', control-openhands browser check 'testid=trust-checkbox', control-openhands service stop agent-server, control-openhands browser click 'testid=start-conversation-button', control-openhands browser wait 'testid=launch-error' --timeout 60000 and control-openhands browser text 'testid=launch-error': Unable to Launch, Failed to Start Conversation, Go Home, Try Again (browser screenshot --feature F19.launch-creation-failed --name error). control-openhands browser click 'testid=try-again-button' reloads the page; with the server still down the app shows Manage backends with the Local backend Disconnected. Restore with control-openhands restart --timeout 240 and control-openhands browser reload: the review modal is back and control-openhands browser eval "document.querySelector('[data-testid=trust-checkbox]').checked" is false. Run control-openhands doctor before continuing.
Non-ASCII deep linkF19.launch-unicodeMapped behavior
Scenario F19--non-ascii-deep-link · Permalink · Source
Given
Use F19’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 /launch?plugins=W3sic291cmNlIjoiZ2l0aHViOnFhL3LDqXBvIn1d.
Expect
a deep link whose JSON contains non-ASCII text (UTF-8 base64) shows that text correctly.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOnFhL3LDqXBvIn1d' (UTF-8 base64 of [{"source":"github:qa/répo"}]) and control-openhands browser text 'testid=plugin-launch-modal >> h2'. Expected Launch qa/répo; today Launch qa/répo.
PhoneF19.phoneMapped behavior
Scenario F19--phone · Permalink · Source
Given
Use F19’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 /plugins.
- DoChoose “Plugins add plugin”.
- DoOpen /customize.
- DoChoose “Extensions mobile hub” / “Sidebar extensions /plugins”.
- DoChoose “Sidebar mobile back”.
- SetupUse the desktop viewport.
Expect
at 390 px the toolbar stacks, cards fit, and the detail, Add and launch modals stay inside the viewport.
Exact CLI recipe & bug checks
Run control-openhands browser viewport phone, control-openhands browser goto /plugins, control-openhands browser bbox 'testid=skills-plugins-screen' (insideViewport true, pageHorizontalOverflow false) and control-openhands browser eval "(()=>{const s=document.querySelector('[data-testid=plugins-search-input]').getBoundingClientRect(), f=document.querySelector('[data-testid=plugins-status-filter]').getBoundingClientRect();return {searchBottom:Math.round(s.bottom), filterTop:Math.round(f.top), filterWidth:Math.round(f.width)}})()": filterTop is below searchBottom (stacked) and filterWidth is 348. control-openhands browser screenshot --feature F19.phone --name list shows one column of cards. Open testid=plugin-card-city-weather and control-openhands browser bbox 'testid=plugin-detail-modal' (insideViewport true, browser screenshot --feature F19.phone --name detail), Escape; control-openhands browser click 'testid=plugins-add-plugin-button' and control-openhands browser bbox 'testid=add-plugin-modal' (insideViewport true), Escape. The parameters link from above gives control-openhands browser bbox 'testid=plugin-launch-modal' insideViewport true (browser screenshot --feature F19.phone --name launch). Hub entry: control-openhands browser goto /customize, control-openhands browser click 'testid=extensions-mobile-hub >> testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)' and control-openhands browser click 'testid=sidebar-mobile-back-button' --expect-url '/customize(\?|$)'. Return with control-openhands browser viewport desktop.
Errors sweepF19.pageMapped behavior
Scenario F19--errors-sweep · Permalink · Source
Given
Use F19’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
- Do
control-openhands browser errors --app-onlyshows no page errors; the only app HTTP errors are the deliberate ones (refresh 500, install 400, 502s while the Agent Server was stopped).
Expect
/plugins shows the Plugins heading, the description "Browse, install, enable, and uninstall plugins. Enabled plugins load automatically into new conversations.", an enabled Add plugin button on a local backend, and the catalog as cards.
Exact CLI recipe & bug checks
control-openhands browser errors --app-only shows no page errors; the only app HTTP errors are the deliberate ones (refresh 500, install 400, 502s while the Agent Server was stopped).
Launch trust shows actual hostF19.launch-trustKnown failure
Scenario F19--launch-trust-shows-actual-host · Permalink · Source
Given
Use F19’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 /launch?plugin_source=https%3A%2F%2Fevil.example%2Fgithub.com%2FAll-Hands-AI%2Fopenhands-plugin.
Expect
Start Conversation stays disabled until the trust checkbox (I trust this plugin from <sources> …) is ticked; then it creates the conversation (sending the message, if any), opens /conversations/<id>, and the plugin is loaded there.
Known bug observations
Known failure, reproduced 2026-10-12 (#17162): it shows All-Hands-AI/openhands-plugin and omits evil.example throughout the dialog. browser enabled 'testid=start-conversation-button' is false. Screenshot, reload and repeat the modal text; leave without ticking trust or starting. This display check does not download or run that source.
Exact CLI recipe & bug checks
Run control-openhands browser goto '/launch?plugin_source=https%3A%2F%2Fevil.example%2Fgithub.com%2FAll-Hands-AI%2Fopenhands-plugin' and browser text 'testid=plugin-launch-modal'. Expected: the trust sentence contains the full https://evil.example/... source. Known failure, reproduced 2026-10-12 (#17162): it shows All-Hands-AI/openhands-plugin and omits evil.example throughout the dialog. browser enabled 'testid=start-conversation-button' is false. Screenshot, reload and repeat the modal text; leave without ticking trust or starting. This display check does not download or run that source.
Unicode parameter round-tripF19.launch-unicodeKnown failure
Scenario F19--unicode-parameter-round-trip · Permalink · Source
Given
Use F19’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
/launch?plugins=W3sic291cmNlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9leGFtcGxlL3BsdWdpbiIsICJwYXJhbWV0ZXJzIjogeyJ0YXNrIjogIuaVtOeQhuWPkeW4g%2BivtOaYjiDwn5qAIn19XQ%3D%3D(UTF-8 base64 of[{"source":"https://github.com/example/plugin","parameters":{"task":"整理发布说明 🚀"}}]). Runbrowser value 'testid=plugin-0-param-task', screenshot and reload/read again. Expected: exactly整理发布说明 🚀. Known failure, reproduced 2026-10-12 (#16735): both reads contain Latin-1 mojibake. Leave without trust or Start. This tests the live launch decoder; the source-side URL builder's exception is a separate branch.
Expect
a deep link whose JSON contains non-ASCII text (UTF-8 base64) shows that text correctly.
Known bug observations
Known failure, reproduced 2026-10-12 (#16735): both reads contain Latin-1 mojibake. Leave without trust or Start. This tests the live launch decoder; the source-side URL builder's exception is a separate branch.
Exact CLI recipe & bug checks
Open /launch?plugins=W3sic291cmNlIjogImh0dHBzOi8vZ2l0aHViLmNvbS9leGFtcGxlL3BsdWdpbiIsICJwYXJhbWV0ZXJzIjogeyJ0YXNrIjogIuaVtOeQhuWPkeW4g%2BivtOaYjiDwn5qAIn19XQ%3D%3D (UTF-8 base64 of [{"source":"https://github.com/example/plugin","parameters":{"task":"整理发布说明 🚀"}}]). Run browser value 'testid=plugin-0-param-task', screenshot and reload/read again. Expected: exactly 整理发布说明 🚀. Known failure, reproduced 2026-10-12 (#16735): both reads contain Latin-1 mojibake. Leave without trust or Start. This tests the live launch decoder; the source-side URL builder's exception is a separate branch.
Cached source rejects an unknown refF19.add-pluginKnown failure
Scenario F19--cached-source-rejects-an-unknown-ref · Permalink · Source
Given
Use F19’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
- DoArrange a run-owned local Git plugin with
.plugin/plugin.jsonnameqa-pin-plugin, a valid skill, tagv1at version 1.0.0 and a later main commit at version 2.0.0. Add itsfile://source with Refv1through the real form and uninstall it, leaving the Git cache warm. Add the same source with Refqa-does-not-exist; inspect the modal, toast and installed version, then repeat after uninstalling. Expected: missing-ref error and no installation. Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5645): the modal closes withPlugin installed.and cached version 1.0.0 is installed. No external Git host or account is required.
Expect
Add plugin opens Add a plugin with Source (required), Ref and Path (optional); Install stays disabled until Source has text; a valid source installs and closes the modal.
Known bug observations
Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5645): the modal closes with Plugin installed. and cached version 1.0.0 is installed. No external Git host or account is required.
Exact CLI recipe & bug checks
Arrange a run-owned local Git plugin with .plugin/plugin.json name qa-pin-plugin, a valid skill, tag v1 at version 1.0.0 and a later main commit at version 2.0.0. Add its file:// source with Ref v1 through the real form and uninstall it, leaving the Git cache warm. Add the same source with Ref qa-does-not-exist; inspect the modal, toast and installed version, then repeat after uninstalling. Expected: missing-ref error and no installation. Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5645): the modal closes with Plugin installed. and cached version 1.0.0 is installed. No external Git host or account is required.
Update retains a requested pinF19.refreshKnown failureKnown issue
Scenario F19--update-retains-a-requested-pin · Permalink · Source
Given
Use F19’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
- DoUsing that own Git fixture, install Ref
v1and privately read only its publicrequested_refmetadata inprivate/plugins/installed/.installed.json; it isv1. Click the card's Update and wait for the completedPlugin updated.toast before reading again. Expected: retainv1. Known failure, reproduced 2026-10-12 (OpenHands/software-agent-sdk#4363): the stored requested ref becomes null. The visible version remained 1.0.0 in this cache, so this diagnostic proves loss of the requested pin, not a default-HEAD version jump. Earlier completed updates also had null; keep that limit explicit and uninstall the fixture.
Expect
Update in an installed plugin's modal re-fetches it from its recorded source and shows Plugin updated.
Known bug observations
Known failure, reproduced 2026-10-12 (OpenHands/software-agent-sdk#4363): the stored requested ref becomes null. The visible version remained 1.0.0 in this cache, so this diagnostic proves loss of the requested pin, not a default-HEAD version jump. Earlier completed updates also had null; keep that limit explicit and uninstall the fixture.
Linked known issues
Known issue OpenHands/software-agent-sdk#5985: Update can re-enable a disabled plugin when an installed-list request overlaps the force-reinstall directory swap. A live check needs an owned disabled plugin and a real concurrent list that lands in that window; the report's monkeypatched copytree schedule is not Canvas live proof. No overlap has been proved in this pass. (F19.refresh, F19.enable-toggle).
Exact CLI recipe & bug checks
Using that own Git fixture, install Ref v1 and privately read only its public requested_ref metadata in private/plugins/installed/.installed.json; it is v1. Click the card's Update and wait for the completed Plugin updated. toast before reading again. Expected: retain v1. Known failure, reproduced 2026-10-12 (OpenHands/software-agent-sdk#4363): the stored requested ref becomes null. The visible version remained 1.0.0 in this cache, so this diagnostic proves loss of the requested pin, not a default-HEAD version jump. Earlier completed updates also had null; keep that limit explicit and uninstall the fixture.
A malformed optional plugin does not block the first messageF19.enabled-autoloadKnown failure
Scenario F19--a-malformed-optional-plugin-does-not-block-the-first-message · Permalink · Source
Given
Use F19’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
- DoArrange a local own plugin directory with an invalid JSON
.plugin/plugin.jsonand open itsplugin_sourcelaunch path. Review/trust only that dummy local fixture and Start; creation yields an idle conversation because plugin loading is deferred. Send a real first message with the dummy loopback model, then read the UI error card,conversation events <id> --kinds SystemPromptEvent --from-start --last 1and status; repeat with a fresh launch. Expected: skip/report the broken optional plugin and continue initialization. Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5718): the UI showsInvalid JSONandSending..., no event/system prompt is persisted and status stays idle. A valid manifest with malformed SKILL.md instead did initialize and reached the refused model endpoint; do not merge those different outcomes. The fixture contains no runnable entry point or external dependency; remove it afterwards.
Expect
an installed, enabled plugin loads into every new conversation; disabled, it does not.
Known bug observations
Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5718): the UI shows Invalid JSON and Sending..., no event/system prompt is persisted and status stays idle. A valid manifest with malformed SKILL.md instead did initialize and reached the refused model endpoint; do not merge those different outcomes. The fixture contains no runnable entry point or external dependency; remove it afterwards.
Exact CLI recipe & bug checks
Arrange a local own plugin directory with an invalid JSON .plugin/plugin.json and open its plugin_source launch path. Review/trust only that dummy local fixture and Start; creation yields an idle conversation because plugin loading is deferred. Send a real first message with the dummy loopback model, then read the UI error card, conversation events <id> --kinds SystemPromptEvent --from-start --last 1 and status; repeat with a fresh launch. Expected: skip/report the broken optional plugin and continue initialization. Known failure, reproduced twice 2026-10-12 (OpenHands/software-agent-sdk#5718): the UI shows Invalid JSON and Sending..., no event/system prompt is persisted and status stays idle. A valid manifest with malformed SKILL.md instead did initialize and reached the refused model endpoint; do not merge those different outcomes. The fixture contains no runnable entry point or external dependency; remove it afterwards.
Disabled plugin reconciles on resumeF19.enabled-autoloadKnown failure
Scenario F19--disabled-plugin-reconciles-on-resume · Permalink · Source
Given
Use F19’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
- DoSend a follow-up with the prompt “/qa-resume-plugin:qa-hello”.
Expect
an installed, enabled plugin loads into every new conversation; disabled, it does not.
Known bug observations
Known failure, reproduced in two independent persisted conversations on 2026-10-12 (OpenHands/software-agent-sdk#5628): the resumed user MessageEvent activated qa-resume-plugin:qa-hello, and both plugin skills remained; the original hook count stayed one, so hooks did follow disable. A fresh chat begun while disabled activated no command and ran no hook. These are real pre-model activation/state observations; no successful reply or plugin MCP reconciliation is claimed. Uninstall only the own plugin and remove its source fixture afterwards.
Exact CLI recipe & bug checks
Arrange an own installed qa-resume-plugin with a valid .plugin/plugin.json, commands/qa-hello.md, skills/qa-plugin-skill/SKILL.md and a UserPromptSubmit hook containing only echo QA_HOOK. Start a real Home chat with the dummy loopback profile; inspect its HookExecutionEvent (one) and plugin skill names through api GET '/api/conversations/<id>?include_skills=true' --pick agent.agent_context.skills (keep the full response private). Disable the real plugin-toggle-qa-resume-plugin with --hover-first 250, verify GET /api/plugins/installed says false, leave Plugins, restart, doctor, reload the conversation and conversation send <id> --prompt '/qa-resume-plugin:qa-hello'. Expected: the disabled command does not activate and plugin skills disappear. Known failure, reproduced in two independent persisted conversations on 2026-10-12 (OpenHands/software-agent-sdk#5628): the resumed user MessageEvent activated qa-resume-plugin:qa-hello, and both plugin skills remained; the original hook count stayed one, so hooks did follow disable. A fresh chat begun while disabled activated no command and ran no hook. These are real pre-model activation/state observations; no successful reply or plugin MCP reconciliation is claimed. Uninstall only the own plugin and remove its source fixture afterwards.
Family notes, prerequisites for known issues & limits Known issue
- The card switch swaps its icon on hover. A plain
browser clickthat moves the pointer onto it from elsewhere (for example right after clicking a filter segment) is often swallowed (#17941): noPATCH /api/plugins/installed/<name>shows inbrowser network. Runbrowser hoveron the switch first, then click, and assertaria-checkedand the API, not the click result. - Installed plugins lose their source and their enabled state, intermittently (OpenHands/software-agent-sdk#5496). The Agent Server rewrites
<run>/private/plugins/installed/.installed.jsonwith a plain truncate-and-write on every load (SDKInstallationMetadata.save_to_dir), so a concurrentGET /api/plugins/installed(page loads fire several) can read an empty file: it logsFailed to load installed extension metadata … Invalid JSON: EOF while parsingandDiscovered untracked extension, then saves every plugin with"source": "local","enabled": trueand a freshinstalled_at. It hit catalog installs in most trials and a disabled plugin on 2 of 3 page revisits; GitHub installs kept their source in this run. Once it hits, the card showslocal, Update fails with a 500, Start Conversation builds an unusable/launchlink, and a disabled plugin silently auto-loads again. Prove the launch path from a card that is not installed, and re-read the API after every navigation. - A failed plugin install, update or uninstall shows one toast with the server's reason (enable uses the same handler; its failure is not driven here). A response with no reason in it, such as the ingress's 502 while the Agent Server is stopped, shows the generic
An error occurred; read the cause in the run'sprivate/stack.log. - Installing a plugin whose name is already installed points the user to Update or Uninstall, as Apps do (
F20.install-duplicate): with the catalogmagic-testinstalled, Add plugin withgithub:OpenHands/extensionsand Pathplugins/magic-testanswers 409 (browser network --filter 'plugins/install') andbrowser toasts --historyholds one toast,This plugin is already installed. Open its card to Update or Uninstall it., instead of the server'sforce=truehint; the modal stays open with the typed values (browser screenshot --feature F19.add-plugin-error --name duplicate-409). Other install errors still toast the server's reason (F19.add-plugin-error). The 409 adds onehttp-errorand oneconsole.errortobrowser errors --app-only. - Uninstall acts immediately, with no confirmation (MCP server delete and App uninstall confirm).
/launchwith amessagestarts the agent as soon as Start Conversation is clicked. Prefer links without a message and sendalakazamyourself; acity-weatherlaunch with a message makes the agent call an external weather API (stop it withtestid=stop-button).- Edited launch parameters cannot be observed after creation: the conversation's client metadata (
localStorage['openhands-agent-server-conversation-metadata']) strips them and the Agent Server does not return a conversation's plugins. Proving delivery needs request-body capture (harness gap). conversation startand launches leave the browser on a conversation page, which has its ownchat-input;browser goto /pluginsbefore the next card step.- Build deep links with URL-encoded base64 (
%2B,%2F,%3D): a raw+decodes to a space and gives the invalid-format error. Non-ASCII text must not be used in payloads (atobdecodes bytes as Latin-1). - The page loads three queries (marketplace, installed, local) behind skeleton rows; wait for
testid=plugins-toolbarbefore counting cards. With no plugins at all the page showsNo plugins found.(plugins-empty), which the bundled catalog makes unreachable; on a Cloud backend Add plugin and card actions are disabled (not driven: no Cloud account). - Local plugins under
~/.agents/pluginsappear on the next page load without a restart; the Agent Server'sHOMEis<run>/private/home. - Known issue OpenHands/software-agent-sdk#5985: Update can re-enable a disabled plugin when an installed-list request overlaps the force-reinstall directory swap. A live check needs an owned disabled plugin and a real concurrent list that lands in that window; the report's monkeypatched
copytreeschedule is not Canvas live proof. No overlap has been proved in this pass. (F19.refresh,F19.enable-toggle).
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.