Backends, Cloud and sharing
F25 · 33 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.
F25 Backends, Cloud and sharing
33 behaviors · 37 scripts & contracts · Maintained source
Entry points & prerequisites
What this family covers
One Canvas can drive several agent servers. The backend selector at the bottom of the sidebar lists every registered backend with a live status dot and switches between them; its footer opens Add Backend (OpenHands Cloud login through an OAuth device flow, or a local/remote agent server by host and API key) and Manage Backends (select, edit, remove, and Cloud "Log back in"). When the active backend is unreachable the whole app is replaced by Manage backends in recovery mode. Cloud backends add organization rows, a Cloud settings link and public sharing of conversations (read-only at /shared/conversations/:id); /oauth/device/verify is the device-approval page. When the page is served with the DigitalOcean Managed Agents bridge (the desktop app's preload, or the web server's own bridge, which the launcher's ingress mounts by default behind the session key), Add Backend gains a Providers tab whose DigitalOcean entry signs in with a personal access token and turns a Managed Agents session into a backend. Everything except the Cloud and DigitalOcean accounts themselves can be driven locally with a second control-openhands stack as the extra backend.
How a user gets here
- Sidebar, bottom: the backend selector (
backend-selector; click or hover) → options, Add Backend (add-backend-menu-item), Manage Backends (manage-backends-menu-item). - Collapsed rail: hover the backend icon (
collapsed-backend-selector-link) → the same footer items. - Phone: hamburger (
sidebar-mobile-menu-toggle) → drawer → backend selector. - Manage backends → Add Backend (
manage-backends-add) opens the same Add form. - Direct URLs: any in-app URL with
?backend=<id>;/oauth/device/verify[?user_code=CODE];/shared/conversations/<id>. - Conversation header name → "..." (
conversation-name >> ellipsis-button) for Public Share (Cloud only). - Reloading with an unreachable active backend opens the recovery gate.
- Onboarding's backend step and the public-mode API-key screen also add backends; they belong to F01. The ChatGPT subscription card is F10 (
F10.subscription) and the provider balance card is F27 (F27.usage-provider-balance).
Given
Preconditions:
- Baseline state (launched, doctored,
onboard --skipdone) andcontrol-openhands llm preset deepseekon this run. - One conversation on this run:
control-openhands conversation start --prompt "Reply with exactly: QA_F25_PONG" --wait --timeout 180; note its<id>. The conversation only has to exist: F25 never reads the reply, so a run that ends inerror(for example a key without balance, saved withllm preset deepseek --no-validate) is enough. - A second stack to add as a backend, started without a browser and never driven directly:
export OH_VERIFY_RUN_2=$(OH_VERIFY_RUN= control-openhands launch --new --build never --no-browser --print-run), thencontrol-openhands status --run "$OH_VERIFY_RUN_2"; itsbaseUrlis<second-url>below and its key is$OH_VERIFY_RUN_2/private/session-key. It has no conversations. - No backend named
QA_SecondorQA_Renamedexists (the selector lists onlyLocal). - Bullets run in order: Add creates
QA_Second, Edit renames it toQA_Renamed, Remove deletes it. - The Managed Agents rows need the bridge, which every
control-openhands launchrun has:control-openhands browser eval "!!window.marsBridge"istrueandcurl -s <baseUrl>/mars/healthanswers{"ok":true,"authRequired":true}.F25.digitalocean-agentsandF25.digitalocean-backend-removalstay blocked without a DigitalOcean account. - Cloud account: where this machine's proxy injects OpenHands Cloud credentials (
curl -s -o /dev/null -w '%{http_code}' https://app.all-hands.dev/api/organizationsis200without a token), the Cloud backend bullet below can approve the device flow in its popup; otherwise it is blocked. Blocked here even with such an account:F25.cloud-log-back-in(needs a logged-out Cloud row), org switching inF25.cloud-org-rows(needs a second organization), the archived and error states ofF25.cloud-sandbox-states,F25.cloud-org-suspended(also needs a suspended OpenHands Enterprise organization or membership),F25.share-publicly(toggle), andF25.locked-cloud, which also needs a launcher flag (see Gotchas).
Open the selectorF25.selector-dropdownKnown failure
Scenario F25--open-the-selector · Permalink · Source
Given
Use F25’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 “Backend selector”.
- DoPress Escape.
- DoHover over “Search commands”.
- DoHover over “Backend selector”.
Expect
clicking (or hovering) the selector opens a listbox of backends, each option named <status> <name> with a status dot, and a footer with Add Backend and Manage Backends.
Linked known issues
Expected: Escape never clears the backend selector. Known failure (reproduced 2026-10-09, 2026-10-12, 2026-10-13 and 2026-10-14, #18233): Escape on the focused backend selector while its menu is closed (a second Escape after the one that closes the menu, and sometimes the Escape that closes Manage Backends opened from it) clears the selection: the closed selector shows the active backend's name as an italic placeholder (browser value 'testid=backend-selector >> role=combobox' is "") and its status dot is gone, so browser wait 'testid=backend-selector >> role=status[name="Disconnected"]' times out although the backend is down; a reload restores it. Reload before a closed-selector status check, as the Edit bullet does. Repro: browser click 'testid=backend-selector', browser press Escape twice (#18233). On 2026-10-12 bug proof verified fix PR #18238 at 77de0ad24 against main f07aebdf5 (two xfail, two xpass, desktop). control-openhands bug run F25.selector-dropdown replays it (desktop, after a reload with the mouse off the selector: a reload under a mouse left on it opens its menu and empties its field, which would look like the bug) and reports xfail while the bug lasts:
sh known-bug F25.selector-dropdown escape-closed
control-openhands browser viewport desktop
control-openhands browser goto /
control-openhands browser hover 'testid=root-layout'
control-openhands browser reload
control-openhands browser wait 'testid=backend-selector >> role=status[name="Connected"]' --timeout 15000
control-openhands assert --field value --equals Local -- browser value 'testid=backend-selector >> role=combobox'
control-openhands browser click 'testid=backend-selector'
control-openhands browser press Escape
control-openhands browser press Escape
control-openhands browser screenshot 'testid=backend-selector' --feature F25.selector-dropdown --name escape-closed
control-openhands assert --field value --equals Local --bug-shows '' -- browser value 'testid=backend-selector >> role=combobox' # bug
Exact CLI recipe & bug checks
From /conversations run control-openhands browser click 'testid=backend-selector' and control-openhands browser snapshot 'testid=backend-selector'. The snapshot shows listbox with option "Connected Local", a separator, button "Add Backend" and button "Manage Backends". Screenshot with control-openhands browser screenshot --feature F25.selector-dropdown --name open. Hover opens it too: browser press Escape, move the pointer away with control-openhands browser hover 'testid=command-menu-trigger' (hovering a selector the pointer is already on does nothing), control-openhands browser hover 'testid=backend-selector', then the same snapshot shows combobox "Local" [expanded] and the two footer buttons.
Add Backend modalF25.add-backend-modalMapped behavior
Scenario F25--add-backend-modal · Permalink · Source
Given
Use F25’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 “Add backend menu item”.
- DoPress Tab.
- DoPress Shift+Tab.
- DoPress q.
- DoPress Escape.
- DoChoose “Add backend close”.
- DoReload the page.
- DoUse browser focus as specified in the exact recipe.
- DoPress ArrowRight.
Expect
Add Backend opens "Choose how you want to connect" with a description, a Learn more link to the deployment docs and the tabs OpenHands Cloud (selected) and Agent-server (plus Providers with the bridge, see the Providers row). It takes keyboard focus on open (the close button), Tab and Shift+Tab wrap inside it, and at desktop width closing returns focus to the backend selector (at phone width the drawer closes with the modal and focus falls back to the page body). Escape does not close it; the X does.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-backend-menu-item' and control-openhands browser snapshot 'testid=add-backend-modal': heading Choose how you want to connect, link Compare Cloud, self-hosted, and Enterprise deployment options (/url: https://docs.openhands.dev/overview/introduction, text Learn more) and tab OpenHands Cloud ... [selected] next to tab Agent-server .... Keyboard focus: control-openhands browser eval "(()=>{const a=document.activeElement;const d=document.querySelector('[data-testid=add-backend-modal]');return {tid:a.getAttribute('data-testid'),inDialog:!!(d&&d.contains(a))}})()" is {tid: "add-backend-close", inDialog: true} right after opening; after eight control-openhands browser press Tab and three browser press Shift+Tab (reading the same eval after each) inDialog stays true (Tab past Advanced wraps to the close button), and browser press q then control-openhands browser eval "document.querySelector('[data-testid=chat-input]').textContent" is "" (nothing reaches the Home composer behind). browser screenshot --feature F25.add-backend-modal --name focus-trap-open. control-openhands browser press Escape leaves browser count 'testid=add-backend-modal' at 1; control-openhands browser click 'testid=add-backend-close' makes it 0, and control-openhands browser eval "!!document.activeElement.closest('[data-testid=backend-selector]')" is true (focus is back on the selector). Keyboard focus in a light theme: arrange control-openhands browser eval "localStorage.setItem('openhands-color-theme','light-plus')" and browser reload, reopen the modal, run control-openhands browser focus 'testid=add-backend-option-cloud' and browser press ArrowRight, then control-openhands browser eval "getComputedStyle(document.activeElement).outlineColor". Expected: the theme's focus colour (--oh-focus, #007ACC in Light+). Connection tabs, the guidance toggle and the DigitalOcean provider choice use the theme focus role and preserve host --oh-focus overrides; default focus contrast is at least 3:1 against base, modal, raised and hovered neutral surfaces in all five themes. Solarized Light uses #227FBE. Check the settled outline after its transition, preserving the 2 px width and existing offsets. Restore with browser eval "localStorage.removeItem('openhands-color-theme')".
Advanced hostF25.cloud-advanced-hostMapped behavior
Scenario F25--advanced-host · Permalink · Source
Given
Use F25’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 “Backend selector”.
- DoChoose “Add backend menu item”.
- DoChoose “Add backend advanced toggle”.
Expect
on the Cloud tab, Advanced reveals a Host field (placeholder https://app.all-hands.dev) for a self-hosted Cloud.
Exact CLI recipe & bug checks
Reopen with browser click 'testid=backend-selector' and browser click 'testid=add-backend-menu-item', run control-openhands browser click 'testid=add-backend-advanced-toggle', then control-openhands browser attr 'testid=add-backend-advanced-toggle' aria-expanded (true) and control-openhands browser snapshot 'testid=add-backend-cloud-panel' (textbox Host with placeholder https://app.all-hands.dev).
Device flow errorF25.cloud-device-flowMapped behavior
Scenario F25--device-flow-error · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “http://127.0.0.1:9” in “Add backend cloud host”.
- DoChoose “Add backend login”.
- WaitWait for “Add backend auth error” to appear.
- DoChoose “Add backend auth retry”.
- DoUse browser tabs as specified in the exact recipe.
- DoUse browser close tab as specified in the exact recipe.
- DoUse browser tab as specified in the exact recipe.
Expect
Connect to OpenHands opens a popup and shows Starting authentication..., then Waiting for authorization... with the verification link and Cancel; Cancel returns to the idle button and closes the popup. An unreachable host shows Failed to start device flow: ... with Try again. Approving the code (adds a backend named OpenHands Cloud) needs a Cloud account.
Exact CLI recipe & bug checks
Run control-openhands browser fill 'testid=add-backend-cloud-host' http://127.0.0.1:9, control-openhands browser click 'testid=add-backend-login-button' --observe 'testid=add-backend-device-flow' and control-openhands browser wait 'testid=add-backend-auth-error' --timeout 15000. The observation goes Starting authentication... → Failed to start device flow: Failed to fetch / Try again, and control-openhands browser network --last 5 shows POST /oauth/device/authorize to http://127.0.0.1:9. control-openhands browser click 'testid=add-backend-auth-retry' starts again and fails the same way. Each attempt leaves an about:blank popup: control-openhands browser tabs, then control-openhands browser close-tab <i> for each extra page and control-openhands browser tab 0. Close the modal with testid=add-backend-close.
Device flow awaiting and CancelF25.cloud-device-flowMapped behavior
Scenario F25--device-flow-awaiting-and-cancel · Permalink · Source
Given
Use F25’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 “Add backend login”.
- DoUse browser tabs as specified in the exact recipe.
- DoChoose “Add backend auth cancel”.
- DoUse browser tabs as specified in the exact recipe.
Expect
Connect to OpenHands opens a popup and shows Starting authentication..., then Waiting for authorization... with the verification link and Cancel; Cancel returns to the idle button and closes the popup. An unreachable host shows Failed to start device flow: ... with Try again. Approving the code (adds a backend named OpenHands Cloud) needs a Cloud account.
Exact CLI recipe & bug checks
Reopen Add Backend (Cloud tab, default host), run control-openhands browser click 'testid=add-backend-login-button' --observe 'testid=add-backend-device-flow' --observe-ms 6000, then control-openhands browser snapshot 'testid=add-backend-device-flow'. If https://app.all-hands.dev is reachable (control-openhands browser network --external --last 5 lists it), the snapshot shows Waiting for authorization..., Browser opened. Complete sign-in to continue. If browser didn't open, visit:, a link https://app.all-hands.dev/oauth/device/verify?user_code=<code> and button Cancel; browser tabs lists a second page on that URL. Screenshot with --feature F25.cloud-device-flow --name awaiting, then control-openhands browser click 'testid=add-backend-auth-cancel': the panel is back to Connect to OpenHands / Advanced and browser tabs lists one page. Do not approve the code here (the Cloud backend bullet does, when it can). If Cloud is unreachable you get the error state of the previous bullet instead.
Agent-server guidanceF25.agent-server-guidanceMapped behavior
Scenario F25--agent-server-guidance · Permalink · Source
Given
Use F25’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 “Add backend option agent server”.
- DoChoose “Add backend local guidance toggle”.
- DoChoose “Add backend location option remote”.
- DoChoose “Add backend remote guidance toggle”.
Expect
the Agent-server tab has a Local / Remote toggle. Local shows a collapsible Before you connect with agent-canvas --backend-only --port 8001 and a local-setup docs link; Remote shows Recommended setup, Connection details and a remote-setup docs link.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-backend-option-agent-server' and control-openhands browser snapshot 'testid=add-backend-agent-server-panel': radiogroup Agent-server location with Local checked, heading Before you connect, textboxes Host Name, Host, API Key and button "Connect" [disabled]. control-openhands browser click 'testid=add-backend-local-guidance-toggle' and browser snapshot 'testid=add-backend-local-guidance' show the code agent-canvas --backend-only --port 8001 and link Read the local backend setup guide. control-openhands browser click 'testid=add-backend-location-option-remote' and control-openhands browser click 'testid=add-backend-remote-guidance-toggle'; browser snapshot 'testid=add-backend-remote-guidance' shows Recommended setup, Connection details and Read the remote backend setup guide.
Connect rules and errorsF25.add-agent-serverKnown issue
Scenario F25--connect-rules-and-errors · Permalink · Source
Given
Use F25’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 “QA_Second” in “Add backend name”.
- DoEnter “127.0.0.1:9” in “Add backend host”.
- DoEnter “wrong-key” in “Add backend api key”.
- DoChoose “Add backend location option local”.
- DoChoose “Add backend submit”.
- DoEnter “” in “Add backend api key”.
- DoChoose “Add backend submit”.
- WaitWait for “Add backend error” to appear.
Expect
Connect stays disabled until Host Name and a valid Host are filled; the API Key field is optional for Connect on Local and required on Remote, but a key-protected host still rejects an empty key. A dead host, an empty key or a wrong key shows Could not connect to <host> with the reason inline (Disconnected (check URL or network) for a dead host, Invalid API key for an empty or wrong key); a reachable host with the right key, entered on the default Local location, adds the backend, makes it active (BM-001) and redirects a /conversations/<id> page to /conversations without an error toast, also on the backend's first activation, without asking the new backend for the old conversation id.
Linked known issues
Known issue OpenHands/software-agent-sdk#5789 (F25.add-agent-server, F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through /api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
Exact CLI recipe & bug checks
Still on Remote: control-openhands browser fill 'testid=add-backend-name' QA_Second, control-openhands browser fill 'testid=add-backend-host' 127.0.0.1:9; control-openhands browser enabled 'testid=add-backend-submit' is false (Remote needs a key). control-openhands browser fill 'testid=add-backend-api-key' wrong-key makes it true. control-openhands browser click 'testid=add-backend-location-option-local' keeps the fields. control-openhands browser click 'testid=add-backend-submit' --observe 'testid=add-backend-submit' shows Checking…, then control-openhands browser text 'testid=add-backend-error' starts Could not connect to http://127.0.0.1:9 / Disconnected (check URL or network). .... Fill add-backend-host with <second-url> and submit again: the error reads Could not connect to <second-url> / Invalid API key. On Local the key is optional for Connect but not for a key-protected host: control-openhands browser fill 'testid=add-backend-api-key' '' leaves control-openhands browser enabled 'testid=add-backend-submit' at true (browser value 'testid=add-backend-api-key' reports length 0), and control-openhands browser click 'testid=add-backend-submit' then control-openhands browser wait 'testid=add-backend-error' --timeout 15000 shows the same Could not connect to <second-url> / Invalid API key (browser screenshot --feature F25.add-agent-server --name local-no-key-error). Close with testid=add-backend-close.
Add the second backendF25.add-agent-serverKnown failureKnown issue
Scenario F25--add-the-second-backend · Permalink · Source
Given
Use F25’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/<id>.
- DoChoose “Backend selector”.
- DoChoose “Add backend menu item”.
- DoChoose “Add backend option agent server”.
- DoEnter “the value specified in the recipe” in “Add backend api key”.
- DoChoose “Add backend submit”.
- DoFollow the onboarding setup in the recipe.
- DoReload the page.
Expect
Connect stays disabled until Host Name and a valid Host are filled; the API Key field is optional for Connect on Local and required on Remote, but a key-protected host still rejects an empty key. A dead host, an empty key or a wrong key shows Could not connect to <host> with the reason inline (Disconnected (check URL or network) for a dead host, Invalid API key for an empty or wrong key); a reachable host with the right key, entered on the default Local location, adds the backend, makes it active (BM-001) and redirects a /conversations/<id> page to /conversations without an error toast, also on the backend's first activation, without asking the new backend for the old conversation id.
Known bug observations
Known failure (reproduced 2026-10-13, #18390): on the run's first activation of QA_Second the list sometimes holds GET <second-url>/api/conversations?ids=<id> (200); record the bullet as fail then (see Gotchas). (--external leaves out this stack's own GET /api/conversations?ids=<id> from the goto /conversations/<id> above, which the request log keeps across page loads; <second-url> is another origin, so a request to it is listed.) The new backend asks its own telemetry consent ("This preference is saved for the local backend “QA_Second”"): run control-openhands onboard --skip (it leaves the browser on /). After control-openhands browser reload the selector still reads QA_Second.
Linked known issues
Known issue OpenHands/software-agent-sdk#5789 (F25.add-agent-server, F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through /api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
Exact CLI recipe & bug checks
Run control-openhands browser goto /conversations/<id>, browser click 'testid=backend-selector', browser click 'testid=add-backend-menu-item', browser click 'testid=add-backend-option-agent-server', fill add-backend-name with QA_Second and add-backend-host with <second-url>, then control-openhands browser fill 'testid=add-backend-api-key' --value-file "$OH_VERIFY_RUN_2/private/session-key" and control-openhands browser click 'testid=add-backend-submit' --expect-url '/conversations(\?|$)'. The URL is /conversations, browser count 'testid=add-backend-modal' is 0, browser snapshot 'testid=backend-selector' shows combobox "QA_Second", and browser count 'testid=conversation-card' is 0. Before anything reloads the page, control-openhands browser toasts --history lists only Loading... and the new backend's Your LLM isn't set up yet, so conversations won't run. ... (the second stack has no LLM profile; no error toast), and control-openhands browser network --external --filter 'ids=' lists no request: the new backend is never asked for the first stack's conversation id. Known failure (reproduced 2026-10-13, #18390): on the run's first activation of QA_Second the list sometimes holds GET <second-url>/api/conversations?ids=<id> (200); record the bullet as fail then (see Gotchas). (--external leaves out this stack's own GET /api/conversations?ids=<id> from the goto /conversations/<id> above, which the request log keeps across page loads; <second-url> is another origin, so a request to it is listed.) The new backend asks its own telemetry consent ("This preference is saved for the local backend “QA_Second”"): run control-openhands onboard --skip (it leaves the browser on /). After control-openhands browser reload the selector still reads QA_Second.
Switch with the overlayF25.switch-backendKnown issue
Scenario F25--switch-with-the-overlay · Permalink · Source
Given
Use F25’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 “Backend selector”.
- DoChoose “Backend selector” / “Connected Local”.
- DoChoose “Conversation card”.
- DoChoose “Backend selector” / “Connected QA_Second”.
Expect
choosing another option shows a full-screen Switching to <name> overlay for about a second, then the sidebar and pages show that backend's data; on /conversations/<id> the app moves to /conversations (BM-002).
Linked known issues
Known issue #15822 (F25.switch-backend F25.health-status F25.cloud-org-rows): Cloud-proxy traffic was reported to continue after switching from Cloud to Local. Current source probes registered inactive Cloud backends on a five-minute cadence; recheck the reported repeated OPTIONS behavior with a real registered Cloud account/org and capture only credential-redacted request metadata after switching. This run has no authenticated Cloud backend.
Exact CLI recipe & bug checks
From / run browser click 'testid=backend-selector' and control-openhands browser click 'testid=backend-selector >> role=option[name="Connected Local"]' --observe 'testid=environment-switch-overlay' --observe-ms 2500. The observation shows Switching to Local, then <absent> about one second later; the selector reads Local and browser count 'testid=conversation-card' is 1. Then control-openhands browser click 'testid=conversation-card' --expect-url '/conversations/[0-9a-f-]+', open the selector and control-openhands browser click 'testid=backend-selector >> role=option[name="Connected QA_Second"]' --expect-url '/conversations(\?|$)': the URL is /conversations, the selector reads QA_Second and the card count is 0.
Pinned URLF25.backend-pinned-urlMapped behavior
Scenario F25--pinned-url · Permalink · Source
Given
Use F25’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/<id>?backend=default-local.
- WaitWait for “Chat interface” to appear.
- DoOpen /conversations?backend=qa-unknown-id.
Expect
in-app links carry ?backend=<id>; opening such a URL pins the tab to that backend; an unknown id is ignored.
Exact CLI recipe & bug checks
With QA_Second active run control-openhands browser goto '/conversations/<id>?backend=default-local' and control-openhands browser wait 'testid=chat-interface': the selector reads Local and the conversation opens. control-openhands browser goto '/conversations?backend=qa-unknown-id' keeps Local (card count 1). The sidebar card links themselves end in ?backend=default-local (browser snapshot).
Per-tab backendF25.per-tab-backendMapped behavior
Scenario F25--per-tab-backend · Permalink · Source
Given
Use F25’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.
- DoUse browser tab as specified in the exact recipe.
- DoUse browser tabs as specified in the exact recipe.
- WaitWait for the element identified in the CLI recipe to appear.
- DoUse browser tab as specified in the exact recipe.
- DoUse browser tab as specified in the exact recipe.
- WaitWait for “Backend selector” to appear.
- DoChoose “Backend selector”.
- DoChoose “Backend selector” / “Connected QA_Second”.
- WaitWait for “Environment switch overlay” to disappear.
- DoUse browser tab as specified in the exact recipe.
- DoUse browser tab as specified in the exact recipe.
- WaitWait for “Backend selector” to appear.
- DoUse browser tab as specified in the exact recipe.
- DoChoose “Conversation card”.
- WaitWait for the element identified in the CLI recipe to appear.
- DoUse browser tab as specified in the exact recipe.
- WaitWait for “Chat interface” to appear.
- DoUse browser close tab as specified in the exact recipe.
- DoUse browser close tab as specified in the exact recipe.
- DoUse browser close tab as specified in the exact recipe.
- DoUse browser tab as specified in the exact recipe.
- DoUse browser tabs as specified in the exact recipe.
Expect
the active backend is per browser tab: switching it in one tab leaves the other open tabs where they were, a new tab opened from a plain link starts from the last choice made in any tab, and a sidebar conversation row opened in a new tab (Control-click) lands on the backend that owns the conversation because its link carries ?backend=<owner>.
Exact CLI recipe & bug checks
With Local active and QA_Second registered, run control-openhands browser goto /conversations and control-openhands browser eval "JSON.parse(sessionStorage.getItem('openhands-active-backend')).backendId" (default-local); note QA_Second's id with control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-backends')).find(b=>b.name==='QA_Second').id" (<second-id>, a generated UUID). Open a plain second tab, as a user opening the app again: control-openhands browser tab new /conversations prints index 1 and makes it the daemon's active tab (control-openhands browser tabs lists two pages at /conversations with active 1); control-openhands browser wait-tab '/conversations$' --new --timeout 5000 then prints index 0, the tab you came from, because --new leaves out the tab the daemon is on and that is now the new one (run from browser tab 0 it prints index 1, the second tab). control-openhands browser tab 1, control-openhands browser wait 'testid=backend-selector': browser snapshot 'testid=backend-selector' reads combobox "Local" and control-openhands browser eval "sessionStorage.getItem('openhands-active-backend')" is null (a new tab inherits nothing and starts from the localStorage fallback). Switch this tab: browser click 'testid=backend-selector', control-openhands browser click 'testid=backend-selector >> role=option[name="Connected QA_Second"]', control-openhands browser wait 'testid=environment-switch-overlay' --state detached --timeout 10000; the selector reads QA_Second, browser count 'testid=conversation-card' is 0 and control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-active-backend')).backendId" is <second-id>. Back in control-openhands browser tab 0 nothing moved: the selector still reads Local, the card count is 1, the sessionStorage eval is still default-local while the localStorage eval is <second-id> (browser screenshot --feature F25.per-tab-backend --name first-tab-unchanged). A plain new tab starts from the last choice made in any tab: from tab 0 run control-openhands browser tab new /conversations (index 2, now active), browser wait 'testid=backend-selector': the selector reads QA_Second, the card count is 0, its sessionStorage item is null and the localStorage eval is <second-id> (--name plain-new-tab-last-choice). A conversation row opens on its owner instead: control-openhands browser tab 0, control-openhands browser click 'testid=conversation-card' --modifiers Control (add >> nth=0 when the run holds more conversations than the precondition's one: the click is strict), control-openhands browser wait-tab '/conversations/<id>' --timeout 20000 prints index 3 and the URL /conversations/<id>?backend=default-local; control-openhands browser tab 3, control-openhands browser wait 'testid=chat-interface' --timeout 30000, browser url still ends in /conversations/<id>?backend=default-local (no bounce to /conversations), the selector reads Local and browser toasts --history lists only Loading... (no This conversation does not exist…); browser screenshot --feature F25.per-tab-backend --name new-tab-on-owner. Close the extra tabs highest first: control-openhands browser close-tab 3, control-openhands browser close-tab 2, control-openhands browser close-tab 1, control-openhands browser tab 0; browser tabs lists one page and the selector reads Local. Opening the pinned link also recorded default-local as the last choice (the localStorage eval in tab 3 reads default-local), so run the plain-new-tab check before the row check.
Manage list and closingF25.manage-backendsMapped behavior
Scenario F25--manage-list-and-closing · Permalink · Source
Given
Use F25’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 “Backend selector”.
- DoChoose “Manage backends menu item”.
- DoPress Escape.
- DoUse browser mouse click as specified in the exact recipe.
- DoChoose “Close manage backends modal”.
- DoChoose “Manage backends done”.
Expect
Manage backends lists every backend with status dot, name, version (v1.50.1), host, status text and a LOCAL/CLOUD pill, plus Edit and Remove; it takes keyboard focus on open and keeps Tab inside; Escape, a backdrop click, X and Done close it and return focus to the selector.
Exact CLI recipe & bug checks
Run browser click 'testid=backend-selector', control-openhands browser click 'testid=manage-backends-menu-item' and control-openhands browser snapshot 'testid=manage-backends-modal': heading Manage backends, two rows Connected Local v1.50.1 <this run's URL> Connected Local and Connected QA_Second v1.50.1 <second-url> Connected Local, each with Edit and Remove, then Add Backend and Done. browser text 'testid=manage-backends-status-QA_Second' is Connected and browser text 'testid=manage-backends-version-QA_Second' is v1.50.1 (whatever doctor reports). Screenshot with --feature F25.manage-backends --name list. Focus is inside: the Add Backend bullet's focus eval with manage-backends-modal in place of add-backend-modal reads close-manage-backends-modal on open and stays inDialog true over nine Tabs and two Shift+Tabs (with two backends listed, Tab 8 reaches Done and Tab 9 wraps to the close button); after Escape, focus is back on the selector. Close it four ways, reopening between: browser press Escape, control-openhands browser mouse-click 20 500 (backdrop), browser click 'testid=close-manage-backends-modal', browser click 'testid=manage-backends-done'; each time browser count 'testid=manage-backends-modal' is 0.
Select from ManageF25.manage-selectMapped behavior
Scenario F25--select-from-manage · Permalink · Source
Given
Use F25’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 “Conversation card”.
- DoChoose “Manage backends row qa second” / the button / match 1.
Expect
clicking a healthy row makes that backend active and closes the modal; on a detail page it leaves it for that section's list, like the selector does (BM-002).
Exact CLI recipe & bug checks
With Local active, control-openhands browser click 'testid=conversation-card' --expect-url '/conversations/[0-9a-f-]+', open Manage Backends, then control-openhands browser click 'testid=manage-backends-row-QA_Second >> role=button >> nth=0' --expect-url '/conversations(\?|$)'. Like the selector, the app leaves the detail page for /conversations (BM-002): browser count 'testid=manage-backends-modal' is 0, the selector reads QA_Second, browser count 'testid=chat-interface' and browser count 'testid=conversation-card' are 0, and browser screenshot --feature F25.manage-select --name after-select shows QA_Second's Home with its empty conversation list.
Edit validation and errorsF25.edit-backendMapped behavior
Scenario F25--edit-validation-and-errors · Permalink · Source
Given
Use F25’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 “Manage backends edit qa second”.
- DoUse browser focus as specified in the exact recipe.
Expect
the pencil opens "Edit backend" pre-filled (key masked) with a Connected · Local v<version> badge; empty name and invalid host show Name is required and Enter a valid URL (e.g. http://localhost:8080) and disable Save; Save re-tests the connection, keeps the modal open with Could not connect to <host> on failure, and persists on success.
Exact CLI recipe & bug checks
Open Manage Backends, control-openhands browser click 'testid=manage-backends-edit-QA_Second', then browser value 'testid=edit-backend-host' (<second-url>), browser attr 'testid=edit-backend-api-key' type (password) and browser text 'testid=edit-backend-status' (Connected, ·, Local, v1.50.1 on separate lines). Fill edit-backend-name with an empty string and edit-backend-host with not a host, then browser focus 'testid=edit-backend-api-key' and browser snapshot 'testid=edit-backend-form': alerts Name is required and Enter a valid URL (e.g. http://localhost:8080); browser enabled 'testid=edit-backend-submit' is false.
Escape in EditF25.edit-escapeKnown failure
Scenario F25--escape-in-edit · Permalink · Source
Given
Use F25’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
- DoPress Escape.
Expect
Escape in the Edit modal should not discard the draft (the modal disables Escape).
Known bug observations
Known failure, see Gotchas.
Linked known issues
Known failure (repro candidate, not an exemption): F25.edit-escape closes the Manage modal underneath and drops the Edit draft (Cancel and X close only Edit, F25.edit-cancel) (#17953).
Exact CLI recipe & bug checks
In the same Edit modal press control-openhands browser press Escape. Expected: the Edit modal stays (it disables Escape) or only it closes. Today browser count 'testid=edit-backend-modal' and browser count 'testid=manage-backends-modal' are both 0: the whole stack closes and the draft is lost. Nothing was saved (browser snapshot 'testid=backend-selector' still reads QA_Second).
Edit and saveF25.edit-backendMapped behavior
Scenario F25--edit-and-save · Permalink · Source
Given
Use F25’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 “Edit backend submit”.
- WaitWait for “Edit backend error” to appear.
- WaitWait for “Edit backend modal” to disappear.
- DoReload the page.
Expect
the pencil opens "Edit backend" pre-filled (key masked) with a Connected · Local v<version> badge; empty name and invalid host show Name is required and Enter a valid URL (e.g. http://localhost:8080) and disable Save; Save re-tests the connection, keeps the modal open with Could not connect to <host> on failure, and persists on success.
Exact CLI recipe & bug checks
Reopen Manage Backends and the pencil. Fill edit-backend-host with http://127.0.0.1:9, browser click 'testid=edit-backend-submit', browser wait 'testid=edit-backend-error' --timeout 10000: the text starts Could not connect to http://127.0.0.1:9 and the modal stays. Fill the host back with <second-url> and edit-backend-name with QA_Renamed, click edit-backend-submit, then control-openhands browser wait 'testid=edit-backend-modal' --state detached --timeout 10000; browser count 'testid=manage-backends-row-QA_Renamed' is 1. Click manage-backends-done, browser reload: the selector reads QA_Renamed.
Health statusF25.health-statusKnown issue
Scenario F25--health-status · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupChange the run-owned service state: stop agent-server --run $OH_VERIFY_RUN_2.
- WaitWait for “Backend selector” / “Disconnected” to appear.
- DoChoose “Backend selector”.
Expect
a backend that stops answering turns its dot red (Disconnected) within one health probe (every 30 s for local and remote agent servers; Cloud backends every 5 min and not on window focus or reconnect, #18128); its Manage row reads Disconnected (check URL or network) with a red detail line and cannot be selected.
Linked known issues
Known issue #15822 (F25.switch-backend F25.health-status F25.cloud-org-rows): Cloud-proxy traffic was reported to continue after switching from Cloud to Local. Current source probes registered inactive Cloud backends on a five-minute cadence; recheck the reported repeated OPTIONS behavior with a real registered Cloud account/org and capture only credential-redacted request metadata after switching. This run has no authenticated Cloud backend.
Known issue OpenHands/software-agent-sdk#5789 (F25.add-agent-server, F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through /api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
Exact CLI recipe & bug checks
With QA_Renamed the active backend (as the Edit bullet's reload leaves it; the closed selector shows only the active backend's dot), run control-openhands service stop agent-server --run "$OH_VERIFY_RUN_2", then on the open page control-openhands browser wait 'testid=backend-selector >> role=status[name="Disconnected"]' --timeout 40000. browser click 'testid=backend-selector' and browser snapshot 'testid=backend-selector' show option "Disconnected QA_Renamed" next to option "Connected Local". In Manage Backends browser text 'testid=manage-backends-status-QA_Renamed' is Disconnected (check URL or network), testid=manage-backends-status-detail-QA_Renamed repeats it with Check that the backend URL is correct ..., and browser enabled 'testid=manage-backends-row-QA_Renamed >> role=button >> nth=0' is false. An error toast with the same text also appears. Screenshot with --feature F25.health-status --name manage-down.
Recovery gateF25.recovery-gateMapped behavior
Scenario F25--recovery-gate · Permalink · Source
Given
Use F25’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.
- WaitWait for “Manage backends modal” to appear.
- DoPress Escape.
- DoChoose “Manage backends row local” / the button / match 1.
- WaitWait for “Agent server onboarding screen” to disappear.
Expect
reloading while the active backend is down shows only agent-server-onboarding-screen with Manage backends in recovery mode (no X, no Done, Escape ignored, primary Add Backend); choosing a healthy row restores the app.
Exact CLI recipe & bug checks
Close the modal, control-openhands browser reload, then control-openhands browser wait 'testid=manage-backends-modal' --timeout 20000. browser testids lists agent-server-onboarding-screen and the Manage rows but no sidebar; browser count 'testid=close-manage-backends-modal' and browser count 'testid=manage-backends-done' are 0, and browser press Escape leaves the modal (count 1). control-openhands browser click 'testid=manage-backends-row-Local >> role=button >> nth=0' and browser wait 'testid=agent-server-onboarding-screen' --state detached --timeout 15000 restore the app: selector Local, card count 1. Screenshot the gate with --feature F25.recovery-gate --name gate before choosing.
Remove the active backendF25.remove-backendMapped behavior
Scenario F25--remove-the-active-backend · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoRestart the run-owned stack as specified.
- DoChoose “Backend selector”.
- WaitWait for “Backend selector” / “Connected QA_Renamed” to appear.
- DoChoose “Manage backends remove qa renamed”.
- DoChoose “Confirmation dialog” / “Cancel”.
- DoChoose “Confirmation dialog” / “Confirm”.
- DoReload the page.
Expect
the trash icon asks Remove backend "<name>"? If it is active, the app will switch back to Local.; Cancel keeps it, Confirm removes it and an active removed backend falls back to Local (BM-003).
Exact CLI recipe & bug checks
Run control-openhands restart --run "$OH_VERIFY_RUN_2", browser click 'testid=backend-selector', browser wait 'testid=backend-selector >> role=option[name="Connected QA_Renamed"]' --timeout 25000 and click that option (selector reads QA_Renamed). Open Manage Backends, control-openhands browser click 'testid=manage-backends-remove-QA_Renamed'; browser text 'testid=confirmation-modal' reads Remove backend "QA_Renamed"? If it is active, the app will switch back to Local. control-openhands browser click 'testid=confirmation-modal >> role=button[name="Cancel"]' keeps the row (count 'testid=manage-backends-row-QA_Renamed' 1). Click the trash again, then control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button': the row count is 0. Click Done and browser reload: the selector reads Local, browser count 'testid=backend-selector >> role=option' (dropdown open) is 1, and the card count is 1.
Add from Manage, Edit Cancel and XF25.manage-add · F25.edit-cancelKnown failure
Scenario F25--add-from-manage-edit-cancel-and-x · Permalink · Source
Given
Use F25’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/<id>.
- WaitWait for “Chat interface” to appear.
- DoChoose “Manage backends add”.
- WaitWait for “Add backend modal” to appear.
- DoChoose “Add backend close”.
- DoChoose “Add backend submit”.
- DoChoose “Manage backends edit qa second”.
- DoEnter “QA_Draft” in “Edit backend name”.
- DoChoose “Edit backend cancel”.
- DoChoose “Edit backend close”.
- DoChoose “Manage backends remove qa second”.
- DoChoose “Confirmation dialog” / “Confirm”.
- DoChoose “Manage backends done”.
- DoReload the page.
Expect
Add Backend in Manage backends opens the same Add form stacked over Manage; its X returns to Manage, and a successful add keeps Manage open with the new row while the new backend becomes active.
Cancel and the X in Edit backend close only the Edit modal and discard the draft; Manage backends stays open with the stored values.
Linked known issues
Known failure (repro candidate, not an exemption): F25.edit-escape closes the Manage modal underneath and drops the Edit draft (Cancel and X close only Edit, F25.edit-cancel) (#17953).
Exact CLI recipe & bug checks
The second stack is running again after the previous bullet. Run browser goto /conversations/<id>, browser wait 'testid=chat-interface', open the selector, click manage-backends-menu-item, then control-openhands browser click 'testid=manage-backends-add' and control-openhands browser wait 'testid=add-backend-modal' --timeout 5000 (visible); browser count 'testid=manage-backends-modal' is 1 underneath. browser click 'testid=add-backend-close': the Add count is 0 and the Manage count stays 1. Click manage-backends-add again, add-backend-option-agent-server, fill add-backend-name with QA_Second, add-backend-host with <second-url> and add-backend-api-key with --value-file "$OH_VERIFY_RUN_2/private/session-key", then browser click 'testid=add-backend-submit' --expect-url '/conversations(\?|$)': the URL is /conversations, count 'testid=add-backend-modal' is 0, count 'testid=manage-backends-modal' is still 1, count 'testid=manage-backends-row-QA_Second' is 1 and the selector reads QA_Second (screenshot --feature F25.manage-add --name after-add). No consent prompt this time: it is stored on the second backend. Then control-openhands browser click 'testid=manage-backends-edit-QA_Second', browser fill 'testid=edit-backend-name' QA_Draft and control-openhands browser click 'testid=edit-backend-cancel': count 'testid=edit-backend-modal' 0, count 'testid=manage-backends-modal' 1, row QA_Second 1, row QA_Draft 0. Repeat with control-openhands browser click 'testid=edit-backend-close': same counts, and reopening the pencil shows browser value 'testid=edit-backend-name' QA_Second; leave with edit-backend-cancel. Remove it again: browser click 'testid=manage-backends-remove-QA_Second', browser click 'testid=confirmation-modal >> testid=confirm-button', browser click 'testid=manage-backends-done' and browser reload: the selector reads Local.
Collapsed railF25.collapsed-entryMapped behavior
Scenario F25--collapsed-rail · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Sidebar collapse toggle”.
- DoHover over “Collapsed backend selector”.
- DoChoose “Add backend menu item”.
- WaitWait for “Add backend modal” to appear.
- WaitWait for “Manage backends modal” to appear.
- DoChoose “Sidebar collapse toggle”.
Expect
with the rail collapsed, the backend icon's popover offers the same Add Backend and Manage Backends items, and both modals open.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=sidebar-collapse-toggle' (attr 'aside[data-collapsed]' data-collapsed is true), control-openhands browser hover 'testid=collapsed-backend-selector-link', control-openhands browser click 'testid=add-backend-menu-item' and control-openhands browser wait 'testid=add-backend-modal' --timeout 5000 (visible). Close it, hover again, click manage-backends-menu-item and browser wait 'testid=manage-backends-modal' --timeout 5000. Close with Done and expand with browser click 'testid=sidebar-collapse-toggle'.
PhoneF25.phoneMapped behavior
Scenario F25--phone · Permalink · Source
Given
Use F25’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 /conversations.
- DoChoose “Sidebar mobile menu toggle”.
- DoChoose “Sidebar mobile drawer” / “Backend selector”.
- DoChoose “Add backend menu item”.
- DoChoose “Add backend option agent server”.
- SetupUse the 320x568 viewport.
- SetupUse the 320x400 viewport.
- DoScroll “Edit backend form” as specified in the recipe.
- SetupUse the desktop viewport.
Expect
at 390 px the drawer's selector opens Add and Manage inside the viewport without horizontal overflow. Edit backend fits at 320 and 390 px; on short viewports its form scrolls while the title and close control stay visible, and Cancel/Save remain reachable.
Exact CLI recipe & bug checks
Run control-openhands browser viewport phone, browser goto /conversations, control-openhands browser click 'testid=sidebar-mobile-menu-toggle', control-openhands browser click 'testid=sidebar-mobile-drawer >> testid=backend-selector', browser click 'testid=add-backend-menu-item', browser click 'testid=add-backend-option-agent-server' and control-openhands browser bbox 'testid=add-backend-modal': insideViewport true, pageHorizontalOverflow false. Screenshot --feature F25.phone --name add-modal. Close it (the drawer stays open), open Manage Backends from the drawer selector the same way and check bbox 'testid=manage-backends-modal' the same. Click testid=manage-backends-edit-Local and check bbox 'testid=edit-backend-modal': insideViewport true; repeat at browser viewport 320x568. At browser viewport 320x400, clear edit-backend-name, fill edit-backend-host with not a host and focus edit-backend-api-key to show both validation messages. Run browser scroll 'testid=edit-backend-form' --by 600: Cancel and Save must come into view while the title and edit-backend-close stay visible. Check the modal and all three controls with bbox, then close with Cancel. Return with browser viewport desktop; reopen Edit and confirm its width is still 520px.
Providers tabF25.providers-tabKnown failure
Scenario F25--providers-tab · Permalink · Source
Given
Use F25’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 “Add backend option providers”.
Expect
only when the page has the Managed Agents bridge (window.marsBridge), Add Backend shows a third tab Providers ("Connect through DigitalOcean and more.") with a DigitalOcean card ("Your team's OpenHands agents") and More providers coming soon.. Every launcher-served web page has it (the ingress answers /mars/health); without it (MARS_WEB=0, or a non-loopback bind with no session key at all) there are only the Cloud and Agent-server tabs and no Managed Agents UI anywhere. A non-loopback bind that has a key (F26 --host 0.0.0.0 with LOCAL_BACKEND_API_KEY) mounts the bridge behind that key and offers the tab too, although the page itself carries no key (see the sign-in bullet's
Known bug observations
Known failure).
Exact CLI recipe & bug checks
On the default run browser eval "!!window.marsBridge" is true (Preconditions); curl -s "http://127.0.0.1:$(control-openhands status | jq -r .ports.frontend)/mars/health" (the static child behind the ingress) returns the HTML index, not JSON: one bridge per stack. Open Add Backend: the tablist adds tab "Providers Connect through DigitalOcean and more." (testid=add-backend-option-providers); control-openhands browser click 'testid=add-backend-option-providers' and browser snapshot 'testid=add-backend-providers-panel' show button "DigitalOcean Your team's OpenHands agents" and More providers coming soon. (browser screenshot --feature F25.add-backend-modal --name with-providers-tab). At phone width the modal stays inside the viewport with the three tab titles cut to Ope…, Age…, Pro… (--feature F25.providers-tab --name phone-tabs).
DigitalOcean sign-inF25.digitalocean-sign-inKnown failure
Scenario F25--digitalocean-sign-in · Permalink · Source
Given
Use F25’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 “Add backend provider digitalocean”.
- WaitWait for “Managed agents sign in” to appear.
- DoChoose “Add backend providers back”.
Expect
the DigitalOcean card opens "Connect your DigitalOcean team" with a Back button, a masked Personal access token field, a Create a token link to https://cloud.digitalocean.com/account/api/tokens and Connect with token, disabled until a token is typed (the Sign in with DigitalOcean OAuth button exists only in the desktop app). A token of the wrong shape reads That does not look like a DigitalOcean personal access token.; a well-formed token DigitalOcean rejects shows its HTTP error (401 Unauthorized). Back returns to the Providers tab.
Known bug observations
Known failure (reproduced 2026-10-14, #18434): every token, even the malformed one, ends in session key required (POST /mars/rpc/getAuthState and savePat 401), because the web bridge client sends only the baked page key, never the typed one; it predates #18349 (BASE with MARS_WEB=1 fails the same way). A --frontend-only page connected to another stack's backend fails the same way for a different reason (the typed key belongs to the other stack); #18434 leaves that case to a maintainer decision.
Exact CLI recipe & bug checks
On the default run, run control-openhands browser click 'testid=add-backend-provider-digitalocean' and browser wait 'testid=managed-agents-sign-in': heading Connect your DigitalOcean team, textbox "Personal access token" (browser attr 'testid=managed-agents-token' type is password), link Create a token (/url: https://cloud.digitalocean.com/account/api/tokens) and button "Connect with token" [disabled]; no OAuth button in a browser (browser count 'testid=managed-agents-oauth' is 0). Fill testid=managed-agents-token with dop_v1_qa_invalid_token_0000 and click testid=managed-agents-token-submit: browser text 'testid=managed-agents-auth-error' reads That does not look like a DigitalOcean personal access token. (POST /mars/rpc/savePat 500). Fill a well-formed dummy, dop_v1_ plus 64 zeros, and submit again: after a few seconds the error reads 401 Unauthorized (POST /mars/rpc/savePat 401, DigitalOcean's answer; browser screenshot --feature F25.digitalocean-sign-in --name rejected-token). Never type a real token here. control-openhands browser click 'testid=add-backend-providers-back' returns to the Providers panel (browser count 'testid=managed-agents-sign-in' is 0; the providers panel itself stays mounted behind the sign-in screen, so its count is 1 either way); close with testid=add-backend-close. At phone width the sign-in screen fits (bbox 'testid=add-backend-modal' insideViewport true). Both errors also raise an error toast with the same text. Expected on a page served without a baked key (F26 --host 0.0.0.0, Docker), once the server's key was typed on the API-key screen: the same two answers as above. Known failure (reproduced 2026-10-14, #18434): every token, even the malformed one, ends in session key required (POST /mars/rpc/getAuthState and savePat 401), because the web bridge client sends only the baked page key, never the typed one; it predates #18349 (BASE with MARS_WEB=1 fails the same way). A --frontend-only page connected to another stack's backend fails the same way for a different reason (the typed key belongs to the other stack); #18434 leaves that case to a maintainer decision.
Managed Agents with an accountF25.digitalocean-agents · F25.digitalocean-backend-removalBlocked
Scenario F25--managed-agents-with-an-account · Permalink · Source
Given
Use F25’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
- DoBlocked: needs a DigitalOcean account with Managed Agents. With one, after a real token the panel lists agents (
digitalocean-agent-<id>), New Agent opensdigitalocean-new-agent-form(name, size), a session's Connect adds a backend and the sidebar showsmars-session-panel; removing that backend opensmars-backend-removal-modalwithdisconnect-only-buttonandstop-session-button.
Expect
signed in, the panel lists the team's OpenHands agents and their sessions, New Agent (name and sandbox size), refresh and sign-out; connecting a session (or Resume and connect a paused one) adds it as the active backend, the sidebar's conversation list becomes that agent's session panel (mars-session-panel), and Manage Backends' Managed Agents view offers Pause, Stop, Disconnect and Open per session (blocked: needs a DigitalOcean account with Managed Agents).
removing a DigitalOcean backend in Manage Backends explains that it only disconnects and offers Disconnect only or Stop session (blocked: needs a connected DigitalOcean session).
Exact CLI recipe & bug checks
Blocked: needs a DigitalOcean account with Managed Agents. With one, after a real token the panel lists agents (digitalocean-agent-<id>), New Agent opens digitalocean-new-agent-form (name, size), a session's Connect adds a backend and the sidebar shows mars-session-panel; removing that backend opens mars-backend-removal-modal with disconnect-only-button and stop-session-button.
Cloud backendF25.cloud-device-flow · F25.cloud-org-rows · F25.cloud-settings-link · F25.share-publicly · F25.cloud-sandbox-statesKnown issueBlocked
Scenario F25--cloud-backend · Permalink · Source
Given
Use F25’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 “Add backend login”.
- DoUse browser tab as specified in the exact recipe.
- DoChoose “Authorize Device”.
- DoUse browser tab as specified in the exact recipe.
- WaitWait for the element identified in the CLI recipe to appear.
- DoUse browser close tab as specified in the exact recipe.
- DoUse browser tab as specified in the exact recipe.
- DoChoose “Submit”.
- DoReload the page.
Expect
Connect to OpenHands opens a popup and shows Starting authentication..., then Waiting for authorization... with the verification link and Cancel; Cancel returns to the idle button and closes the popup. An unreachable host shows Failed to start device flow: ... with Try again. Approving the code (adds a backend named OpenHands Cloud) needs a Cloud account.
each Cloud backend appears once per organization as <name> – <org> (Personal Workspace for the user's own), and choosing one scopes lists to that org.
with a Cloud backend active the gear next to the selector opens <cloud host>/settings?org=<org> in a new tab.
on a Cloud backend the conversation menu has a Public Share toggle with copy-link and open-link buttons; on a local backend the item is absent.
Cloud conversations show waiting, archived and error sandbox states in chat.
Linked known issues
Known issue #17390 (F25.cloud-sandbox-states): Every sent Cloud message can fail with token_not_found_in_db from the model proxy. Recheck with the affected Cloud account/session and a minimal no-tools prompt; capture the error type with provider token text and hashes redacted. The report supplies no deployment version or deterministic token-provisioning sequence, and this run has no Cloud account.
Known issue #17163 (F25.cloud-sandbox-states): A Free Cloud account can lose access to previously usable models with HTTP 403 and a team-restriction message despite no team being created. Recheck with the affected account and its model entitlement/org history; local credentials cannot prove Cloud eligibility and this run has no such account.
Known issue #16874 (F25.cloud-sandbox-states): A failed resume of a paused Cloud sandbox was reported to consume a one-shot guard and leave chat disconnected until remount. Current source uses a retrying resume hook, so recheck the original failure and a second pause in one route mount before concluding resolution. Blocked without a real paused Cloud sandbox and a controlled one-request backend/network failure; source changes alone are not a live pass.
Known issue #15676 (F25.cloud-sandbox-states): Refreshing an approximately 24-hour-old Cloud conversation on Vercel can fail to resume/reconnect. Recheck an authenticated affected Cloud deployment with an aged sandbox/session and exact console/network error; the report omits its commit and failure text, and no aged Cloud fixture is available here.
Known issue #15822 (F25.switch-backend F25.health-status F25.cloud-org-rows): Cloud-proxy traffic was reported to continue after switching from Cloud to Local. Current source probes registered inactive Cloud backends on a five-minute cadence; recheck the reported repeated OPTIONS behavior with a real registered Cloud account/org and capture only credential-redacted request metadata after switching. This run has no authenticated Cloud backend.
Known issue OpenHands/automation#565 (F25.cloud-org-rows, F25.cloud-org-suspended): An org-scoped Cloud automation request with an unauthorized X-Org-Id can turn the identity service's 403 into a misleading 502/outage page. Recheck with an authenticated Cloud org-bound key and a disposable unauthorized organization id, comparing automation list and the identity response while preserving credential redaction. Local mode bypasses this identity service and cannot reproduce it.
Exact CLI recipe & bug checks
Only with a Cloud account (Preconditions); it creates a device grant and one conversation on that account, and shows the account's own conversations in the sidebar (keep those screenshots private). Reopen Add Backend on the Cloud tab, browser click 'testid=add-backend-login-button', then control-openhands browser tab 1 (the verify popup: Device Authorization Request, the code, Authorize Device), control-openhands browser click 'role=button[name="Authorize Device"]' (Success!) and control-openhands browser tab 0. Within about 15 s (token polling answers 400 until approved, then 200) browser count 'testid=add-backend-modal' is 0, the popup is gone and browser snapshot 'testid=backend-selector' reads combobox "OpenHands Cloud": OpenHands Cloud – Personal Workspace; the registry holds {name: "OpenHands Cloud", host: "https://app.all-hands.dev", kind: "cloud"} (browser eval "JSON.parse(localStorage.getItem('openhands-backends')).map(b=>({name:b.name,host:b.host,kind:b.kind}))"). Open the selector: option "Connected OpenHands Cloud – Personal Workspace" (one option per organization) next to Connected Local; Manage Backends shows the row with Personal Workspace and the Cloud pill. browser attr 'testid=backend-selector-settings-link' href is https://app.all-hands.dev/settings?org=<org id> with target _blank; clicking it opens that tab (browser wait-tab 'app.all-hands.dev/settings', then browser close-tab 1, browser tab 0). Start one conversation from Home: fill testid=chat-input with Reply with exactly: QA_F25_CLOUD. Do not run any tools. and browser click 'testid=submit-button' --expect-url '/conversations/': the URL is /conversations/task-<id> while the sandbox starts (toasts Creating conversation…, Sending...), then moves to /conversations/<id>?backend=<cloud id>&org=<org id> and the reply QA_F25_CLOUD appears (about a minute). Its conversation-name >> ellipsis-button menu has share-publicly-button (Public Share, off); do not flip it unless the conversation may be public. Delete it with delete-button and 'role=dialog >> testid=confirm-button', then remove the Cloud backend in Manage Backends (manage-backends-remove-OpenHands Cloud, confirm): after browser reload the selector reads Local.
Device verify pageF25.device-verify-pageMapped behavior
Scenario F25--device-verify-page · Permalink · Source
Given
Use F25’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 /oauth/device/verify.
- DoEnter “QA-0000” in “Device Code:”.
- DoChoose “Continue”.
- DoOpen /oauth/device/verify?user_code=QA-0000.
- DoChoose “Cancel”.
- DoChoose “Authorize Device”.
- WaitWait for “Error” to appear.
Expect
/oauth/device/verify shows a "Device Authorization" code form; with ?user_code= it shows "Device Authorization Request", the code, a Security Notice and Cancel / Authorize Device. On a local stack Authorize and Continue end in the Error card with Try Again.
Exact CLI recipe & bug checks
Run control-openhands browser goto /oauth/device/verify and browser snapshot: heading Device Authorization, Enter the code displayed on your device:, textbox Device Code: and button Continue, inside the normal app shell. control-openhands browser fill 'role=textbox[name="Device Code:"]' QA-0000 and browser click 'role=button[name="Continue"]' go straight to the result: heading Error, Failed to authorize device. Please try again., button Try Again; browser network --filter verify-authenticated shows POST /oauth/device/verify-authenticated 404. Then control-openhands browser goto '/oauth/device/verify?user_code=QA-0000': heading Device Authorization Request, DEVICE CODE, QA-0000, Security Notice, buttons Cancel and Authorize Device (screenshot --feature F25.device-verify-page --name with-code). browser click 'role=button[name="Cancel"]' does nothing in a tab the page did not open (URL unchanged). browser click 'role=button[name="Authorize Device"]' and browser wait 'role=heading[name="Error"]' show the same Error card; Try Again reloads to the request.
CleanupF25 · setup / cleanupSupport step
Scenario F25--cleanup · Permalink · Source
Given
Use F25’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
- DoPress Escape.
- DoStop the run-owned stack.
Expect
Complete this support step before continuing to the next scenario.
Exact CLI recipe & bug checks
browser press Escape, then control-openhands stop --run "$OH_VERIFY_RUN_2" and unset OH_VERIFY_RUN_2. The conversation stays with this run's state.
ErrorsF25 · setup / cleanupSupport step
Scenario F25--errors · Permalink · Source
Given
Use F25’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-onlyafter the family shows no page errors. Expected app-origin entries:POST /oauth/device/verify-authenticated404 (twice), the shared-view 404s, the warningScripts may close only the windows that were opened by them.from the device-verify Cancel, and a burst of CORSconsole.errors for<second-url>while the second agent server is stopped or restarting (see Gotchas).
Expect
Complete this support step before continuing to the next scenario.
Exact CLI recipe & bug checks
control-openhands browser errors --app-only after the family shows no page errors. Expected app-origin entries: POST /oauth/device/verify-authenticated 404 (twice), the shared-view 404s, the warning Scripts may close only the windows that were opened by them. from the device-verify Cancel, and a burst of CORS console.errors for <second-url> while the second agent server is stopped or restarting (see Gotchas).
Same-origin credential readabilityF25.manage-backends · F25.add-agent-serverKnown failureKnown issue
Scenario F25--same-origin-credential-readability · Permalink · Source
Given
Use F25’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
- DoIn an authenticated private run, use read-only
browser evalto return only the count of registered entries and the count whoseapiKeyis a nonempty string inJSON.parse(localStorage.getItem('openhands-backends') || '[]'). Expected: backend credentials are not readable by arbitrary same-origin script. Known failure, reproduced 2026-10-12: registered entries have readable plaintext keys (#16492). Retain counts/booleans only, never key values or whole storage JSON.
Expect
Manage backends lists every backend with status dot, name, version (v1.50.1), host, status text and a LOCAL/CLOUD pill, plus Edit and Remove; it takes keyboard focus on open and keeps Tab inside; Escape, a backdrop click, X and Done close it and return focus to the selector.
Connect stays disabled until Host Name and a valid Host are filled; the API Key field is optional for Connect on Local and required on Remote, but a key-protected host still rejects an empty key. A dead host, an empty key or a wrong key shows Could not connect to <host> with the reason inline (Disconnected (check URL or network) for a dead host, Invalid API key for an empty or wrong key); a reachable host with the right key, entered on the default Local location, adds the backend, makes it active (BM-001) and redirects a /conversations/<id> page to /conversations without an error toast, also on the backend's first activation, without asking the new backend for the old conversation id.
Known bug observations
Known failure, reproduced 2026-10-12: registered entries have readable plaintext keys (#16492). Retain counts/booleans only, never key values or whole storage JSON.
Linked known issues
Known issue OpenHands/software-agent-sdk#5789 (F25.add-agent-server, F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through /api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
Exact CLI recipe & bug checks
In an authenticated private run, use read-only browser eval to return only the count of registered entries and the count whose apiKey is a nonempty string in JSON.parse(localStorage.getItem('openhands-backends') || '[]'). Expected: backend credentials are not readable by arbitrary same-origin script. Known failure, reproduced 2026-10-12: registered entries have readable plaintext keys (#16492). Retain counts/booleans only, never key values or whole storage JSON.
Manually registered same-origin key rotationF25.add-agent-server · F25.recovery-gateKnown failureKnown issue
Scenario F25--manually-registered-same-origin-key-rotation · Permalink · Source
Given
Use F25’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.
Expect
Connect stays disabled until Host Name and a valid Host are filled; the API Key field is optional for Connect on Local and required on Remote, but a key-protected host still rejects an empty key. A dead host, an empty key or a wrong key shows Could not connect to <host> with the reason inline (Disconnected (check URL or network) for a dead host, Invalid API key for an empty or wrong key); a reachable host with the right key, entered on the default Local location, adds the backend, makes it active (BM-001) and redirects a /conversations/<id> page to /conversations without an error toast, also on the backend's first activation, without asking the new backend for the old conversation id.
reloading while the active backend is down shows only agent-server-onboarding-screen with Manage backends in recovery mode (no X, no Done, Escape ignored, primary Add Backend); choosing a healthy row restores the app.
Known bug observations
Known failure, reproduced 2026-10-12: default-local matches the injected key, the manually registered entry does not, and the UI shows Disconnected/Unauthorized (#17371). This version has visible status feedback, so do not repeat the older claim of a completely silent loop. Choose Connected Local to recover, then remove the owned duplicate.
Linked known issues
Known issue OpenHands/software-agent-sdk#5789 (F25.add-agent-server, F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through /api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
Exact CLI recipe & bug checks
Through Add Backend → Agent-server, add an owned entry named QA_Rotation_Legacy with the run's exact origin and fill its API Key using --value-file <run>/private/session-key; keep it active. Run restart --rotate-key, doctor, and browser reload. Compare only each saved entry's apiKey === window.__AGENT_CANVAS_SESSION_API_KEY__ boolean. Expected: same-origin entries accept the newly injected session key and the active backend recovers. Known failure, reproduced 2026-10-12: default-local matches the injected key, the manually registered entry does not, and the UI shows Disconnected/Unauthorized (#17371). This version has visible status feedback, so do not repeat the older claim of a completely silent loop. Choose Connected Local to recover, then remove the owned duplicate.
Non-loopback default backend nameF25.selector-dropdownKnown failure
Scenario F25--non-loopback-default-backend-name · Permalink · Source
Given
Use F25’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
- DoServe the unchanged frontend on an owned non-loopback LAN address with
--auth-required, no session-key injection and real protected backend routes. Open that origin in a new browser tab with--allow-external; its Add a backend step defaults Host Name toLocal. Authenticate only with the private run key file, continue once and skip the remaining setup, then inspectbackend-selector. Expected: the default name identifies the backend's host on a remote origin. Known failure, reproduced 2026-10-12: the selector reads Connected Local on the non-loopback origin (#16297). This is a LAN-origin check, not a VM deployment claim. Stop only the owned fixture frontend and return to the original tab.
Expect
clicking (or hovering) the selector opens a listbox of backends, each option named <status> <name> with a status dot, and a footer with Add Backend and Manage Backends.
Known bug observations
Known failure, reproduced 2026-10-12: the selector reads Connected Local on the non-loopback origin (#16297). This is a LAN-origin check, not a VM deployment claim. Stop only the owned fixture frontend and return to the original tab.
Linked known issues
Expected: Escape never clears the backend selector. Known failure (reproduced 2026-10-09, 2026-10-12, 2026-10-13 and 2026-10-14, #18233): Escape on the focused backend selector while its menu is closed (a second Escape after the one that closes the menu, and sometimes the Escape that closes Manage Backends opened from it) clears the selection: the closed selector shows the active backend's name as an italic placeholder (browser value 'testid=backend-selector >> role=combobox' is "") and its status dot is gone, so browser wait 'testid=backend-selector >> role=status[name="Disconnected"]' times out although the backend is down; a reload restores it. Reload before a closed-selector status check, as the Edit bullet does. Repro: browser click 'testid=backend-selector', browser press Escape twice (#18233). On 2026-10-12 bug proof verified fix PR #18238 at 77de0ad24 against main f07aebdf5 (two xfail, two xpass, desktop). control-openhands bug run F25.selector-dropdown replays it (desktop, after a reload with the mouse off the selector: a reload under a mouse left on it opens its menu and empties its field, which would look like the bug) and reports xfail while the bug lasts:
sh known-bug F25.selector-dropdown escape-closed
control-openhands browser viewport desktop
control-openhands browser goto /
control-openhands browser hover 'testid=root-layout'
control-openhands browser reload
control-openhands browser wait 'testid=backend-selector >> role=status[name="Connected"]' --timeout 15000
control-openhands assert --field value --equals Local -- browser value 'testid=backend-selector >> role=combobox'
control-openhands browser click 'testid=backend-selector'
control-openhands browser press Escape
control-openhands browser press Escape
control-openhands browser screenshot 'testid=backend-selector' --feature F25.selector-dropdown --name escape-closed
control-openhands assert --field value --equals Local --bug-shows '' -- browser value 'testid=backend-selector >> role=combobox' # bug
Exact CLI recipe & bug checks
Serve the unchanged frontend on an owned non-loopback LAN address with --auth-required, no session-key injection and real protected backend routes. Open that origin in a new browser tab with --allow-external; its Add a backend step defaults Host Name to Local. Authenticate only with the private run key file, continue once and skip the remaining setup, then inspect backend-selector. Expected: the default name identifies the backend's host on a remote origin. Known failure, reproduced 2026-10-12: the selector reads Connected Local on the non-loopback origin (#16297). This is a LAN-origin check, not a VM deployment claim. Stop only the owned fixture frontend and return to the original tab.
Cloud log back inF25.cloud-log-back-inBehavior contract
Scenario F25--contract-f25-cloud-log-back-in · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
a logged-out Cloud row shows Logged out and a Log back in button that reruns the device flow.
Cloud org suspendedF25.cloud-org-suspendedKnown issueBlocked
Scenario F25--contract-f25-cloud-org-suspended · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
when a Cloud call made for the selected organization (X-Org-Id) fails with 403 and detail Organization is suspended or User membership is suspended, the app is replaced by <org> is suspended. Contact your administrator to restore access. (or Your access to <org> is suspended. Contact your administrator to restore it.), then Switch to another workspace and one button per other organization (Personal Workspace for the user's own). A button selects that organization and the app returns; with no other organization only the message shows. A reload clears the in-memory record. Blocked here: needs an OpenHands Enterprise instance with ENABLE_SUPER_ADMIN where a Super Admin suspended the organization or this member's membership, signed in as a non-Super-Admin member with that organization selected (#17988).
Linked known issues
Known issue OpenHands/automation#565 (F25.cloud-org-rows, F25.cloud-org-suspended): An org-scoped Cloud automation request with an unauthorized X-Org-Id can turn the identity service's 403 into a misleading 502/outage page. Recheck with an authenticated Cloud org-bound key and a disposable unauthorized organization id, comparing automation list and the identity response while preserving credential redaction. Local mode bypasses this identity service and cannot reproduce it.
Locked cloudF25.locked-cloudKnown issue
Scenario F25--contract-f25-locked-cloud · Permalink · Source
Given
Use F25’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
a Canvas served with --lock-to-cloud <url> shows only the Cloud login (no close) on first run, and its selector footer reads Reconnect to Cloud with no add, edit or remove.
Linked known issues
Known issue #18029 (F25.locked-cloud): A returning user in a locked Cloud deployment can reach the no-backend placeholder and fail to start conversations despite a healthy configured backend. The report proposes a stale document or alternate origin but does not capture which occurred. Recheck against the affected locked deployment with a Cloud session, its exact origin, cache response headers and injected host state; the normal local launcher does not reproduce this deployment condition.
Family notes, prerequisites for known issues & limits Known failureKnown issueBlocked
- Export the second run as
OH_VERIFY_RUN_2and pass it with--run; never export it asOH_VERIFY_RUN, or the browser commands look for a browser that run does not have.OH_VERIFY_RUN= control-openhands launch --new ...keeps the launch from reusing this run. - The second backend asks its own telemetry consent the first time it becomes active, over whatever page you are on;
onboard --skipanswers it. Consent is stored per backend, so a laterrestartof the second run does not ask again. browser fill <sel> --value-file FILEtakes no positional value; it keeps the key out of the shell history and the evidence. Edit forms show the stored key masked, butbrowser snapshotof the Edit form prints it in clear: do not save that snapshot as evidence with a real key.- Manage-row test ids use the backend name (
manage-backends-row-QA_Second), so they change after a rename and are not unique when two backends share a name; the Cloud Log back in id uses the backend id instead (manage-backends-login-<id>-login-button). - Selector options are named
<status> <name>(Connected Local,Disconnected QA_Renamed): select them withrole=option[name="Connected <name>"]. - After a navigating option click,
--expect-urlmust allow?backend=...: end regexes with(\?|$). browser wait-tab <regex>matches any open page, including the current one, unless--newleaves out the tab the daemon is on: for a second tab at the same URL (/conversationsfrom New Chat) usewait-tab '/conversations$' --new, or readbrowser tabsand switch withbrowser tab <i>.browser tab new /conversationsopens a plain second tab (no opener, empty sessionStorage), the state a user reaches by opening the app again. Close extra tabs highest index first, or the indexes shift under you.browser tab newswitches the daemon to the tab it opened, so await-tab <regex> --newrun right after it reports the tab you came from (index0with two tabs);browser tab 0first when you want the new tab's index fromwait-tab. Atab newand aclick --modifiers Controlon New Chat reach the same state (selector from the localStorage fallback, sessionStorage itemnull).- A tab opened with
click --modifiers Controlstarts with an empty sessionStorage, so it boots from the localStorage fallback (the backend last chosen in any tab) unless its URL carries?backend=. Opening a?backend=link writes that backend to localStorage too: a per-tab check that relies on "last choice" must run before opening pinned links. - The Add Backend Agent-server form has no inline "required" messages; it only disables Connect. The messages exist in the Edit form.
- Footer clicks from the collapsed popover or the phone drawer open the modals a moment later:
browser waitfor them instead ofcount. - The device-flow popup survives errors: every failed attempt leaves an
about:blanktab (#17953). Cancel closes it; after an error close it withbrowser close-tab. - With the default host the device flow talks to the real
app.all-hands.dev(POST /oauth/device/authorize, then token polling). Starting and cancelling is harmless; approve a code only in the Cloud backend bullet, with an account you may use. - While the second stack's agent server is stopped or restarting, its ingress answers without CORS headers, so the browser logs
Access to fetch at '<second-url>/api/settings' ... blocked by CORS policy(also/server_info,/api/conversations/search,/api/llm/models/verified). That is the dead upstream, not a CORS bug; a healthy second stack sendsaccess-control-allow-originfor this origin. - Expected: Escape never clears the backend selector. Known failure (reproduced 2026-10-09, 2026-10-12, 2026-10-13 and 2026-10-14, #18233): Escape on the focused backend selector while its menu is closed (a second Escape after the one that closes the menu, and sometimes the Escape that closes Manage Backends opened from it) clears the selection: the closed selector shows the active backend's name as an italic placeholder (
browser value 'testid=backend-selector >> role=combobox'is"") and its status dot is gone, sobrowser wait 'testid=backend-selector >> role=status[name="Disconnected"]'times out although the backend is down; a reload restores it. Reload before a closed-selector status check, as the Edit bullet does. Repro:browser click 'testid=backend-selector',browser press Escapetwice (#18233). On 2026-10-12bug proofverified fix PR #18238 at 77de0ad24 against main f07aebdf5 (twoxfail, twoxpass, desktop).control-openhands bug run F25.selector-dropdownreplays it (desktop, after a reload with the mouse off the selector: a reload under a mouse left on it opens its menu and empties its field, which would look like the bug) and reportsxfailwhile the bug lasts:
sh known-bug F25.selector-dropdown escape-closed
control-openhands browser viewport desktop
control-openhands browser goto /
control-openhands browser hover 'testid=root-layout'
control-openhands browser reload
control-openhands browser wait 'testid=backend-selector >> role=status[name="Connected"]' --timeout 15000
control-openhands assert --field value --equals Local -- browser value 'testid=backend-selector >> role=combobox'
control-openhands browser click 'testid=backend-selector'
control-openhands browser press Escape
control-openhands browser press Escape
control-openhands browser screenshot 'testid=backend-selector' --feature F25.selector-dropdown --name escape-closed
control-openhands assert --field value --equals Local --bug-shows '' -- browser value 'testid=backend-selector >> role=combobox' # bug
- Local and remote backends are probed every 30 s (settings + server_info), Cloud backends every 5 min (#18128), so allow up to 40 s for a local dot to change.
browser network --clear, wait about 70 s, thenbrowser network --filter server_infoshows two probes per local origin 30 s apart. While the active backend is down, any reload lands on the recovery gate: drive the disconnected rows on the page that was already loaded. /oauth/device/verifyis a Cloud page that also ships in Canvas: on a local stack its POST target does not exist, so Authorize always ends inError. The "Authentication Required" state is unreachable (useIsAuthedalways resolvestrue)./shared/conversations/<id>on a local backend also raises a raw toastHTTP request failed (404 Not Found): {"detail":"Not Found"}next toConversation not found(#17953).- Known failure (repro candidate, not an exemption):
F25.edit-escapecloses the Manage modal underneath and drops the Edit draft (Cancel and X close only Edit,F25.edit-cancel) (#17953). - The add bullet's
network --external --filter 'ids='check guards an earlier race: #18390 tracks the same first-activation symptom; on a backend's first activation from/conversations/<id>, about 1 attempt in 5 asked the new backend for the old conversation id. It did not happen in 15 first activations at 4eff80237: each timeQA_Secondwas removed, its consent unset withcontrol-openhands api PATCH /api/settings --run "$OH_VERIFY_RUN_2" --write --data '{"misc_settings_diff":{"app_preferences":{"user_consents_to_analytics":null}}}'(arrange only) and the backend added again from/conversations/<id>. A backend that lacks an id answers[null], which now reads as "not found" (only theThis conversation does not exist…toast), not as incompatible data. No merged change was shown to fix the race itself (#18009 only changed how a[null]answer reads; #18024 only moved the redirect into a shared hook, still called afteraddBackend), so a hit (anids=request to<second-url>) is a bug to file. At 95115e84f6dd it hit on 1 of 2 first activations from/conversations/<id>(oneGET <second-url>/api/conversations?ids=<id>answered200, with noThis conversation does not exist…toast after it). The hit followed a failed Connect in the same modal (an empty key,Invalid API key) before the successful one; the clean retry, which connected once, had none, so a repro should try both sequences. On 2026-10-08 at d2c89252d it did not reproduce in 17 first activations (8 clean, 9 failed-then-clean, some under CPU load and some human-paced): everyids=capture was empty, and no merged change touched the activation logic since the hit, so it stays a watch item rather than a filed defect. Record the Add bullet asfailwhen the request appears, even though the redirect and the selector look right. Known failure (reproduced 2026-10-13, #18390): two independent first activations hit it that day, each the run's first activation ofQA_Secondafter a clean Connect. On 2026-10-13 at 2bdc0e9d6 it hit on 1 of 6 first activations from/conversations/<id>: the first one of the run (GET <second-url>/api/conversations?ids=<id>200, no toast), a clean Connect in a modal opened after earlier failed Connects in other Add Backend openings; the next 5 (2 failed-then-clean, 3 clean, plus one Manage → Add) had none. - Add Backend and Manage Backends take and keep keyboard focus, and Edit backend takes it on open (
ModalBackdrop; at phone width open the selector with'testid=sidebar-mobile-drawer >> testid=backend-selector', since the bare test id matches two selectors). At desktop width focus returns to the selector input when they close (at phone width Add Backend's X closes the drawer too and focus falls to the page body), so an Escape pressed after closing lands on the selector and can trigger #18233 below. - Toasts are not page or HTTP errors, so
browser errors --app-onlymisses them: readbrowser toasts --historyafter every add or switch, before anything reloads the page (the history starts with each page load). F25.locked-cloudneedsscripts/static-server.mjs --lock-to-cloud <url>, which neitherbin/agent-canvas.mjsnorcontrol-openhands launchcan pass today; even with it, reconnecting needs a Cloud account.- The Cloud tab title truncates to
OpenHan…at 390 px when the Agent-server tab is selected; the layout still fits. With the Providers tab present all three titles are cut to three letters. - A token DigitalOcean rejects surfaces as the bare
401 Unauthorized, with no hint that the token is wrong or lacks scopes. - Known issue #18029 (
F25.locked-cloud): A returning user in a locked Cloud deployment can reach the no-backend placeholder and fail to start conversations despite a healthy configured backend. The report proposes a stale document or alternate origin but does not capture which occurred. Recheck against the affected locked deployment with a Cloud session, its exact origin, cache response headers and injected host state; the normal local launcher does not reproduce this deployment condition. - Known issue #17390 (
F25.cloud-sandbox-states): Every sent Cloud message can fail withtoken_not_found_in_dbfrom the model proxy. Recheck with the affected Cloud account/session and a minimal no-tools prompt; capture the error type with provider token text and hashes redacted. The report supplies no deployment version or deterministic token-provisioning sequence, and this run has no Cloud account. - Known issue #17163 (
F25.cloud-sandbox-states): A Free Cloud account can lose access to previously usable models with HTTP 403 and a team-restriction message despite no team being created. Recheck with the affected account and its model entitlement/org history; local credentials cannot prove Cloud eligibility and this run has no such account. - Known issue #16874 (
F25.cloud-sandbox-states): A failed resume of a paused Cloud sandbox was reported to consume a one-shot guard and leave chat disconnected until remount. Current source uses a retrying resume hook, so recheck the original failure and a second pause in one route mount before concluding resolution. Blocked without a real paused Cloud sandbox and a controlled one-request backend/network failure; source changes alone are not a live pass. - Known issue #15676 (
F25.cloud-sandbox-states): Refreshing an approximately 24-hour-old Cloud conversation on Vercel can fail to resume/reconnect. Recheck an authenticated affected Cloud deployment with an aged sandbox/session and exact console/network error; the report omits its commit and failure text, and no aged Cloud fixture is available here. - Known issue #15822 (
F25.switch-backendF25.health-statusF25.cloud-org-rows): Cloud-proxy traffic was reported to continue after switching from Cloud to Local. Current source probes registered inactive Cloud backends on a five-minute cadence; recheck the reported repeated OPTIONS behavior with a real registered Cloud account/org and capture only credential-redacted request metadata after switching. This run has no authenticated Cloud backend. - Known issue OpenHands/automation#565 (
F25.cloud-org-rows,F25.cloud-org-suspended): An org-scoped Cloud automation request with an unauthorizedX-Org-Idcan turn the identity service's 403 into a misleading 502/outage page. Recheck with an authenticated Cloud org-bound key and a disposable unauthorized organization id, comparing automation list and the identity response while preserving credential redaction. Local mode bypasses this identity service and cannot reproduce it. - Known issue OpenHands/software-agent-sdk#5789 (
F25.add-agent-server,F25.health-status): a deferred-init Agent Server can report ready after accepting a GUI origin through/api/init, while browser preflights from that origin still fail and Canvas cannot connect. Recheck an owned warm-pool server initialized with the actual non-localhost Canvas origin and compare admission with an origin supplied at server boot. The default loopback-origin stack is already allowed and cannot exercise this branch.
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.