Engel’s notebook

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.

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: /customize shows 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 --skip done), 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/installed returns {"plugins": []}.
  • The catalog comes from the launcher's cached public-skills checkout and needs no network: control-openhands api GET /api/plugins/marketplace lists city-weather, magic-test and others with local-path source values. magic-test answers "alakazam" with The 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 run H="$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

  1. Do
    Open /conversations.
  2. Do
    Choose “Sidebar skills”.
  3. Do
    Choose “Sidebar extensions /plugins”.
  4. Wait
    Wait 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.

Command-menu entryF19.pageMapped behavior

Scenario F19--command-menu-entry · 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

  1. Do
    Press Control+k.
  2. Do
    Enter “Plugins” in “Command menu” / the combobox.
  3. Do
    Press Enter.
  4. Wait
    Wait for the element identified in the CLI recipe to appear.
  5. Do
    Choose “Sidebar extensions /plugins”.

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

From /conversations run control-openhands browser press Control+k, control-openhands browser type 'testid=command-menu >> role=combobox' Plugins, control-openhands browser snapshot 'testid=command-menu' (one option, Customize Browse skills, plugins, and integrations. Go), control-openhands browser press Enter, control-openhands browser wait-url '/mcp(\?|$)', then control-openhands browser click 'testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)'.

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

  1. Do
    Choose “Plugins filter installed”.
  2. Do
    Choose “Plugins filter available”.
  3. Do
    Choose “Plugins filter local”.
  4. Do
    Reload 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

  1. Do
    Choose “Plugin card magic test”.
  2. Do
    Choose “Plugin detail modal close”.
  3. Do
    Use browser focus as specified in the exact recipe.
  4. Do
    Press Enter.
  5. Do
    Press Escape.
  6. Do
    Use browser focus as specified in the exact recipe.
  7. Do
    Press Space.
  8. Do
    Choose “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

  1. Do
    Choose “File tree dir .plugin”.
  2. Do
    Choose “File tree file .plugin/plugin.json”.
  3. Wait
    Wait 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

  1. Do
    Choose “Plugin install magic test”.
  2. Wait
    Wait for “Plugin toggle magic test” to appear.
  3. Do
    Reload the page.
  4. Do
    Choose “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

  1. Do
    Hover over “Plugin toggle magic test”.
  2. Do
    Choose “Plugin toggle magic test”.
  3. Do
    Reload the page.
  4. Do
    Choose “Plugin card magic test”.
  5. Do
    Choose “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

  1. Do
    Start a conversation with the prompt “alakazam”.
  2. Do
    Open /plugins.
  3. Do
    Start a conversation as specified in the recipe.
  4. Do
    Open /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

  1. Do
    Choose “Plugin card magic test”.
  2. Do
    Choose “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

  1. Do
    Choose “Plugin card magic test”.
  2. Do
    Choose “Plugin detail start conversation magic test”.
  3. Do
    Use browser check as specified in the exact recipe.
  4. Do
    Choose “Start conversation”.
  5. Do
    Enter “alakazam” in “Message”.
  6. Do
    Press 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

  1. Do
    Open /plugins.
  2. Do
    Choose “Plugin card magic test”.
  3. Do
    Choose “Plugin detail uninstall magic test”.
  4. Do
    Reload 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

  1. Do
    Choose “Plugin card city weather”.
  2. Do
    Choose “Plugin detail install city weather”.
  3. Wait
    Wait for “Plugin detail uninstall city weather” to appear.
  4. Do
    Choose “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

  1. Do
    Choose “Plugins add plugin”.
  2. Do
    Enter “qa-typed” in “Add plugin source”.
  3. Do
    Choose “Add plugin modal close”.
  4. Do
    Choose “Add plugin modal dismiss”.
  5. Do
    Press Escape.
  6. Do
    Use 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

  1. Do
    Choose “Plugins add plugin”.
  2. Do
    Enter “ ” in “Add plugin source”.
  3. Do
    Enter “qa-not-a-source” in “Add plugin source”.
  4. Do
    Choose “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

  1. Do
    Enter “github:OpenHands/extensions” in “Add plugin source”.
  2. Do
    Enter “plugins/magic-test” in “Add plugin repo path”.
  3. Do
    Choose “Add plugin submit”.
  4. Wait
    Wait 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

  1. Do
    Choose “Plugins add plugin”.
  2. Do
    Enter “github:OpenHands/extensions” in “Add plugin source”.
  3. Do
    Enter “main” in “Add plugin ref”.
  4. Do
    Enter “plugins/city-weather” in “Add plugin repo path”.
  5. Do
    Choose “Add plugin submit”.
  6. Wait
    Wait for “Add plugin modal” to disappear.
  7. Do
    Choose “Plugin card city weather”.
  8. Do
    Choose “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

  1. Do
    Reload the page.
  2. Do
    Choose “Plugins filter local”.
  3. Do
    Choose “Plugin card qa local plugin”.
  4. Do
    Reload 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

  1. Do
    Choose “Plugin card magic test”.
  2. Do
    Choose “Plugin detail start conversation magic test”.
  3. Do
    Use browser check as specified in the exact recipe.
  4. Do
    Choose “Start conversation”.
  5. Do
    Choose “Chat plus”.
  6. Do
    Choose “Show plugins”.
  7. Do
    Enter “alakazam” in “Message”.
  8. Do
    Press Enter then --selector then testid=chat-input.
  9. Wait
    Wait 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

  1. Do
    Open /launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIifSx7InNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9PcGVuSGFuZHMvZXh0ZW5zaW9ucy5naXQiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL21hZ2ljLXRlc3QiLCJyZWYiOiJtYWluIn1d.
  2. Do
    Open /launch?plugin_source=github%3AOpenHands%2Fextensions&message=%3Cb%3EWeather%3C%2Fb%3E%20for%20QA.
  3. Do
    Choose “Plugin launch modal” / “Close”.
  4. Do
    Press 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

  1. Do
    Open /launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIiLCJyZWYiOiJtYWluIiwicGFyYW1ldGVycyI6eyJjaXR5IjoiUGFyaXMiLCJkYXlzIjozLCJtZXRyaWMiOnRydWV9fV0%3D.
  2. Do
    Choose “Plugin section 0”.
  3. Do
    Enter “Berlin” in “Plugin 0 param city”.
  4. Do
    Enter “” in “Plugin 0 param days”.
  5. Do
    Use 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

  1. Do
    Open /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

  1. Do
    Open <url>.
  2. Do
    Choose “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

  1. Do
    Open /launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_repo_path=plugins%2Fmagic-test.
  2. Do
    Use browser check as specified in the exact recipe.
  3. Setup
    Change the run-owned service state: stop agent-server.
  4. Do
    Choose “Start conversation”.
  5. Wait
    Wait for “Launch error” to appear.
  6. Do
    Choose “Try again”.
  7. Do
    Restart the run-owned stack as specified.
  8. Do
    Reload 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.

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

  1. Setup
    Use the phone viewport.
  2. Do
    Open /plugins.
  3. Do
    Choose “Plugins add plugin”.
  4. Do
    Open /customize.
  5. Do
    Choose “Extensions mobile hub” / “Sidebar extensions /plugins”.
  6. Do
    Choose “Sidebar mobile back”.
  7. Setup
    Use 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

  1. Do
    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).

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

  1. Do
    Open /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

  1. Do
    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.

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

  1. Do
    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.

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

  1. Do
    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.

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

  1. Do
    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.

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

  1. Do
    Send 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.

Unavailable plugin source is a handled Update errorF19.refreshKnown failureKnown issue

Scenario F19--unavailable-plugin-source-is-a-handled-update-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

  1. Do
    Expected:400 for a vanished source, with the installation unchanged. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5897): two real Update clicks after moving only the owned source aside showed the missing-path error and returned500; installed version/enabled state stayed unchanged. This is the plugin branch; Canvas has no skill-management Refresh action.

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 twice 2026-10-16, OpenHands/software-agent-sdk#5897): two real Update clicks after moving only the owned source aside showed the missing-path error and returned500; installed version/enabled state stayed unchanged. This is the plugin branch; Canvas has no skill-management Refresh action.

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

Expected:400 for a vanished source, with the installation unchanged. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5897): two real Update clicks after moving only the owned source aside showed the missing-path error and returned500; installed version/enabled state stayed unchanged. This is the plugin branch; Canvas has no skill-management Refresh action.

sh known-bug F19.refresh missing-source

  QA_PLUGIN_SOURCE="$OH_VERIFY_RUN/workspace/qa-known-refresh-plugin"
  mkdir -p "$QA_PLUGIN_SOURCE/.plugin"
  printf '%s\n' '{"name":"qa-known-refresh-plugin","version":"0.1.0","description":"Inert disposable Update check."}' > "$QA_PLUGIN_SOURCE/.plugin/plugin.json"
  trap 'if [ -d "$QA_PLUGIN_SOURCE.hidden" ]; then mv "$QA_PLUGIN_SOURCE.hidden" "$QA_PLUGIN_SOURCE"; fi; control-openhands api DELETE /api/plugins/installed/qa-known-refresh-plugin --write >/dev/null; rm -rf "$QA_PLUGIN_SOURCE"; control-openhands browser reload >/dev/null' EXIT
  control-openhands browser viewport desktop
  control-openhands browser goto /plugins
  control-openhands browser click 'testid=plugins-add-plugin-button'
  control-openhands browser fill 'testid=add-plugin-source-input' "$QA_PLUGIN_SOURCE"
  control-openhands browser click 'testid=add-plugin-submit'
  control-openhands browser wait 'testid=add-plugin-modal' --state hidden --timeout 30000
  control-openhands assert --field status --equals 200 -- api GET /api/plugins/installed/qa-known-refresh-plugin
  control-openhands browser click 'testid=plugin-card-qa-known-refresh-plugin'
  mv "$QA_PLUGIN_SOURCE" "$QA_PLUGIN_SOURCE.hidden"
  control-openhands browser network --clear
  control-openhands browser click 'testid=plugin-detail-refresh-qa-known-refresh-plugin' --observe 'role=status' --observe-ms 1000
  control-openhands assert --field recent.0.status --equals 400 --bug 'recent.0.status eq 500 && recent.0.path eq "/api/plugins/installed/qa-known-refresh-plugin/refresh"' -- browser network --filter '/api/plugins/installed/qa-known-refresh-plugin/refresh'  # bug
Family notes, prerequisites for known issues & limits Known issue
  • The card switch swaps its icon on hover. A plain browser click that moves the pointer onto it from elsewhere (for example right after clicking a filter segment) is often swallowed (#17941): no PATCH /api/plugins/installed/<name> shows in browser network. Run browser hover on the switch first, then click, and assert aria-checked and 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.json with a plain truncate-and-write on every load (SDK InstallationMetadata.save_to_dir), so a concurrent GET /api/plugins/installed (page loads fire several) can read an empty file: it logs Failed to load installed extension metadata … Invalid JSON: EOF while parsing and Discovered untracked extension, then saves every plugin with "source": "local", "enabled": true and a fresh installed_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 shows local, Update fails with a 500, Start Conversation builds an unusable /launch link, 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's private/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 catalog magic-test installed, Add plugin with github:OpenHands/extensions and Path plugins/magic-test answers 409 (browser network --filter 'plugins/install') and browser toasts --history holds one toast, This plugin is already installed. Open its card to Update or Uninstall it., instead of the server's force=true hint; 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 one http-error and one console.error to browser errors --app-only.
  • Uninstall acts immediately, with no confirmation (MCP server delete and App uninstall confirm).
  • /launch with a message starts the agent as soon as Start Conversation is clicked. Prefer links without a message and send alakazam yourself; a city-weather launch with a message makes the agent call an external weather API (stop it with testid=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 start and launches leave the browser on a conversation page, which has its own chat-input; browser goto /plugins before 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 (atob decodes bytes as Latin-1).
  • The page loads three queries (marketplace, installed, local) behind skeleton rows; wait for testid=plugins-toolbar before counting cards. With no plugins at all the page shows No 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/plugins appear on the next page load without a restart; the Agent Server's HOME is <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 copytree schedule 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.