Commit Graph
7610 Commits
Author SHA1 Message Date
Tim O'Farrellandopenhands 4fa822d1f7 chore: bump openhands-automation to 1.0.0a6 (#1097)
* chore: bump openhands-automation to 1.0.0a6

* Remove fragile automation version assertion test

The test hardcoded an exact version string that breaks on every
version bump. It provides no utility — the version is already
covered by integration/config tests; pinning it in a unit test
just means a manual edit is required on every release.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-03 20:03:03 +00:00
098f8893ba Polish onboarding modal flow and final-step UX (#1095)
* Restore choose-agent as the first onboarding step.

Put agent selection back ahead of backend setup so new users pick an agent before connecting a server, and keep the step-0 Next button right-aligned when Back is hidden.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add onboarding step preview via query param.

Support `?previewOnboardingStep=0-3` to open a specific slide for design review without marking onboarding complete, mounting from the root layout when the param is present.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Redesign onboarding Say Hello step input and footer.

Use the chat-style send control in a raised input container, replace the launch button with Close, and add trailing punctuation to the default hello message across locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Scroll recommended automations grid in onboarding Say Hello.

Keep the section heading fixed while only the workflow cards scroll, and use a lighter raised surface on automation tiles for contrast against the modal panel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make onboarding backend Next save and align the footer.

Remove the separate Save button, left-align Back with Next on the right, and submit the backend form through Next so a successful connection test advances the flow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Disable backdrop dismiss on onboarding modal only.

Add a closeOnBackdropClick option to ModalBackdrop and turn it off for new-user setup so outside clicks no longer skip onboarding while other modals keep the default behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Collapse onboarding backend fields when connected and refresh copy.

Hide the configuration form behind a Show configuration toggle once the health probe succeeds, and explain that users can register multiple agent backends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Left-align Back on the onboarding LLM step footer.

Use justify-between so Back and Next match the layout on the other onboarding steps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Avoid skeleton flicker during onboarding LLM step transition.

Keep SDK settings content visible on background refetches so clicking Next from LLM setup no longer flashes the loading skeleton.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish onboarding footer spacing and suppress setup save toast.

Match the Say Hello footer top/bottom spacing and disable the generic settings-saved toast while advancing through the startup modal LLM step.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refine final onboarding step actions and scrolling.

Hide Skip on the final slide, add an OR divider in Say Hello, and keep the recommendation heading/description inside the same scrollable area as the automation cards.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove extra spacing before onboarding recommendations.

Drop the top margin above the recommended automations block on the Say Hello step so it aligns tightly with the updated scrollable recommendations layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: remove unrelated file

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-04 02:45:07 +07:00
Rohit Malhotraandopenhands 24d627b88b fix: public mode auth gate bypasses stored backend keys (#1101)
* fix: public mode auth gate bypasses stored backend keys

After a user enters their API key via the auth screen in public mode
(`--public`), the key is persisted to the backend registry in localStorage.
On page reload, `isAuthRequiredAndMissing()` only checked for a *baked-in*
session key (env var / window global), which is intentionally absent in
public mode. This caused the auth screen to reappear in an infinite loop.

The fix adds a secondary check: if auth is required and the baked key is
missing, but the backend registry already has a registered backend with an
API key, the instant gate is bypassed and the normal `/server_info` probe
validates the stored key instead.

Also adds:
- E2E test for returning-user scenario (valid stored key → skip auth)
- E2E test for frontend-only → backend-only cross-connection
- E2E test for switching between multiple backend-only instances

Fixes #1099

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: reload page after adding backend in cross-connect tests

The MissingAgentServerScreen's useConfig caches the
AgentServerUnavailableError with retries disabled. After adding a
backend through the manage-backends modal, a page reload is needed
to re-probe with the newly stored backend.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: cross-connect test strict mode violation

Both home-chat-launcher and onboarding-step-choose-agent can render
simultaneously (onboarding overlays the home page). The .or() locator
fails Playwright strict mode when both match. Check each individually
and also assert the error screens are NOT visible.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-03 15:27:04 -04:00
Hiep Le db40530370 fix: keep modal open on errant outside click (#1100) 2026-06-03 18:53:50 +00:00
Hiep Le a344117a2b fix: default LLM setup to Anthropic Claude Opus 4.8 (#1089) 2026-06-04 01:26:46 +07:00
chuckbutkusandopenhands 578589af7f fix(static-server): expose runtime session key via window global (#1091)
The published `agent-canvas` binary is built with no `VITE_SESSION_API_KEY`
baked in. At runtime, `scripts/static-server.mjs --session-api-key <key>`
injects the key into `index.html`, but only as a write to
`localStorage["openhands-agent-server-config"].sessionApiKey`.

PR #1046 ("Simplify backend registry selection") removed the legacy
localStorage fallback from `getAgentServerSessionApiKey()`, leaving it
reading only `import.meta.env.VITE_SESSION_API_KEY`. The combination
broke the first-launch experience for users who installed the npm
package globally:

  1. Fresh browser → `localStorage["openhands-backends"]` is null.
  2. `readLegacyBackend()` requires `baseUrl` AND `sessionApiKey` in
     `openhands-agent-server-config`; the injection only writes the key,
     so it returns null.
  3. `makeDefaultLocalBackend()` reads `VITE_SESSION_API_KEY` → null →
     returns null.
  4. Registry seeds empty → `MissingAgentServerScreen` renders the
     Manage Backends modal ("No extra backends added yet.") with no
     way out.

The fix mirrors how `__AGENT_CANVAS_AUTH_REQUIRED__` already passes the
public-mode flag from `static-server.mjs` to the bundle:

  - `static-server.mjs` now also injects
    `window.__AGENT_CANVAS_SESSION_API_KEY__ = <key>` in the `<head>`
    script. The window global is set first so it is available even if
    the localStorage write throws (private mode, etc.).
  - `getBakedSessionApiKey()` in `src/api/agent-server-config.ts` falls
    back to the window global when `VITE_SESSION_API_KEY` is empty.

Tests:
  - Unit tests in `__tests__/api/agent-server-config.test.ts` cover the
    window-global fallback, env-takes-precedence ordering, whitespace,
    and non-string injected values.
  - `__tests__/scripts/static-server.test.ts` asserts the new window
    assignment lands before the localStorage write.
  - A new mock-LLM E2E spec in
    `tests/e2e/mock-llm/mock-llm-auth-modes.spec.ts` exercises the
    exact user scenario: a fresh browser context with no pre-seeded
    localStorage must reach the onboarding modal (not the Manage
    Backends trap modal) when launched against the prebuilt stack.
  - Updated docstrings in the auth-modes spec and helper to remove
    references to the deleted `syncBakedSessionApiKey()` function.

AGENTS.md updated to document the window-global path and the current
key-rotation reconciliation (`syncLauncherDefaultLocalBackend()` in
`storage.ts`).

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-03 13:46:25 -04:00
Robert Brennan 547a5eb687 Update Docker image version in README (#1070) 2026-06-03 12:29:30 -04:00
b9d78d3116 Simplify backend registry selection (#1046)
* Add partial stack modes to agent-canvas

Co-authored-by: openhands <openhands@all-hands.dev>

* Simplify backend registry selection

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix backend selection CI regressions

* Seed local proxy backend in cloud tests

* Stabilize onboarding snapshot navigation

* Stabilize ingress tests on Windows

* Remove local backend fallback for cloud calls

* Fix frontend-only backend proxy target

* Test backend-only launch without build

* Add pending workflow and status updates

* Use active LLM profile for setup banner

* Show backend connection errors before saving

* Validate backend keys in health checks

* Update backend selector health test mock

* Fix frontend-only workspace path

* Preserve OpenHands proxy base URL

* Address backend review comments

* Preserve profile config when switching models

* Fail fast on profile export errors

* Throw AgentServerUnavailableError when backend registry is empty

When no backend is configured (empty registry / NO_BACKEND sentinel),
loadAgentServerInfo() was returning null without throwing, causing
OptionService.getConfig() to succeed silently. root.tsx then rendered
the home page instead of the MissingAgentServerScreen with the manage
backends modal.

Now loadAgentServerInfo() checks for the NO_BACKEND sentinel when
getEffectiveLocalBackend() returns null and throws
AgentServerUnavailableError, which root.tsx already handles by showing
the manage backends modal. The cloud-backend path (also null from
getEffectiveLocalBackend) is preserved — it still returns null.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: Remove PR-only artifacts

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-03 15:26:51 +00:00
3122fa9a56 feat(acp): render running tool-call cards (pairs with SDK source dedup) (#1027)
* feat(acp): render running tool-call cards (pairs with SDK source dedup)

The SDK now persists exactly two ACPToolCallEvents per tool_call_id -- an early
`started` event (pending/in_progress) and one terminal completed/failed event,
the action->observation pair -- instead of one cumulative-output frame per
ToolCallProgress. Relax shouldRenderEvent so the `started` event renders the
card as "running" (the absent check mark, via getACPToolCallResult) and the
terminal event replaces it in place through handleEventForUI, mirroring how an
ObservationEvent supersedes its ActionEvent.

The old terminal-only gate existed to hide the half-formed cards the SDK's
per-progress fan-out flashed mid-stream; that fan-out is gone, so the running
card is now a single clean event. Widen ACPToolCallStatus to include `pending`
(the started event's status) and refresh the related comments and tests.

Implements OpenHands/agent-canvas#1025 (epic #988). Requires the software-agent-sdk
PR that dedups ACP tool-call progress at the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync getACPToolCallResult doc with pending status (#1027)

Address review nit: the JSDoc mentioned only in_progress, but pending now
also falls through to undefined (running card) after the status-type widening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: address PR review feedback (#1027)

- handle-event-for-ui comment: mention pending alongside in_progress as a
  started-event status (mirrors the widened ACPToolCallStatus type)
- add getACPToolCallResult unit tests (pending/in_progress/null -> undefined,
  failed/is_error -> error, completed -> success)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: reflow handle-event-for-ui comment (#1027)

Address review nit: merge the awkwardly-wrapped sentence onto continuous
lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:31:44 +00:00
Kirtan Manoj Chandak 37c8987247 feat: adds translations for "Remote" in multiple languages in translation.json (#1056) 2026-06-03 11:14:04 +00:00
58b03f3b73 fix: surface dynamic_context in Agent Tools & Metadata modal (#932) (#973)
* fix: surface dynamic_context in Agent Tools & Metadata modal (#932)

The backend SystemPromptEvent carries three content fields
(system_prompt, tools, dynamic_context), but the frontend only read the
first two. dynamic_context (datetime, skills catalog, runtime services,
custom secrets) is part of the system message the model actually
receives and is larger than the static prompt, so the modal showed less
than half of the agent's true context.

- Add optional dynamic_context to the SystemPromptEvent interface
- Surface it via adaptSystemMessage and a new "Dynamic Context" tab
- Defensively redact unmasked <CUSTOM_SECRETS> values client-side
- Reset to the System tab on open so a now-hidden tab can't leave an
  empty panel
- Add tests and translations for all locales

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: remove "Dynamic Context" tab and consolidate context in "System" tab

* refactor: trim system_prompt text and localize regex instances in redactCustomSecrets

---------

Co-authored-by: burak.keles <burak.keles@ode.al>
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
Co-authored-by: simonrosenberg <157206163+simonrosenberg@users.noreply.github.com>
2026-06-03 09:36:41 +00:00
d128a3ef0e feat: expose event trigger details in automation UI (#1007)
* feat: expose event trigger details in automation UI

Add first-class support for displaying event-based automation triggers
in the frontend, closing the gap between what the backend returns and
what users can see.

Changes:
- Extend AutomationTrigger type with source, on, filter fields
- ConfigurationSection shows event source, event type, and filter
  expression (with expand/collapse for long filters)
- Edit modal displays read-only event trigger info block for event
  automations while keeping cron schedule editor for cron automations
- List/card views show type-specific icons (clock for cron, globe for
  event) and event trigger pills instead of schedule pills
- Add formatEventOn shared utility to avoid duplication
- Add i18n translations for 5 new keys across 15 languages
- Add mock event-trigger automations and run history for dev/testing
- Add 5 new tests for ConfigurationSection covering both trigger types

* fix: hide plugins section for event-triggered automations

Per review feedback — plugins section is redundant for event automations
since the event source is already displayed in the configuration section.

* refactor: address review feedback on event trigger details

* fix: failing tests

---------

Co-authored-by: Jathin Sreenivas <sjathin@amazon.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-03 16:16:33 +07:00
5eb519c014 refactor(acp): stop forwarding acp_env; route ACP creds via Secrets panel (#1050)
ACP provider credentials already flow through the Secrets panel
(request.secrets / agent_context.secrets) — the cipher-protected,
wire-native channel that matches the regular agent. acp_env was a
parallel, partly-unmasked credential channel that the SDK has since
deprecated (software-agent-sdk #3464).

Remove acp_env from the forwarded ACP settings so it is no longer sent
on the conversation request, and explicitly scrub any legacy persisted
acp_env from the OpenHands payload (it previously rode the shared
ACP_SETTINGS_KEYS strip-list). Cred delivery is unchanged — the ACP
onboarding already provisions provider keys via the Secrets panel.

Toward the registry-only end-state in OpenHands/agent-canvas#1039
(remove the acp_env channel). Follows OpenHands/agent-canvas#1022.

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:02:36 +00:00
8bb2b8c518 Add frontend-only and backend-only agent-canvas modes (#1040)
* Add partial stack modes to agent-canvas

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address PR review feedback (#1040)

- Replace padEnd(75) with ANSI-aware ansiPadEnd helper in printBanner;
  String.padEnd counts invisible escape bytes as visible chars, causing
  the box border to misalign in colour terminals
- Deduplicate storage directory creation in ensureDirectories; was being
  pushed once per launchAgentServer block and once per launchAutomation
  block (always both true together) — move to a shared unconditional slot
- Add explanatory comment to the checkNpm two-clause OR condition

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add e2e tests for --frontend-only, --backend-only, and port conflicts

Add mock-llm-partial-stack.spec.ts with three test groups:

1. --frontend-only: verifies static frontend is served (200 on /),
   backend routes return 503 (/server_info, /api/settings,
   /api/automation/v1), and the browser shows the manage-backends modal.

2. --backend-only: verifies /server_info returns 200, /api/settings
   is reachable, automation endpoint works, and root/asset requests
   return 503 (no frontend configured).

3. Port conflict: verifies the process exits non-zero with a clear
   error when the ingress port is occupied, then starts successfully
   on a free port.

Unlike the other mock-llm specs, these tests spawn their own
bin/agent-canvas.mjs child processes with isolated state dirs and
high port numbers (18310+ range) to avoid collisions.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: adjust frontend-only test to expect SPA fallback instead of 503

In frontend-only mode the ingress has no backend routes — all requests
(including /server_info, /api/*) fall through to the static server's
default backend, which returns index.html via SPA fallback (200 with
HTML). The test now verifies that /server_info returns HTML (not JSON)
and that the browser detects the missing backend and shows the
manage-backends modal.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: add --reject-prefix to static server for clean 503 on missing backends

In --frontend-only mode the static server was SPA-fallbacking API paths
(/server_info, /api/*, /sockets, etc.) to index.html, returning 200
with HTML content instead of a clear failure. This made the frontend's
/server_info probe ambiguous.

Add a --reject-prefix flag to static-server.mjs: any matching request
returns 503 ('Service Unavailable') before the SPA fallback runs.

Wire it through dev-with-automation.mjs: getRejectPrefixes(config)
computes which API prefixes have no backend configured (e.g. all of
them in frontend-only mode) and buildRejectPrefixArgs() passes them
as --reject-prefix flags to the static server.

Restore the e2e test to assert 503 for /server_info, /api/settings,
and /api/automation/v1 in frontend-only mode.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: treat non-401 HTTP errors from /server_info as unavailable

loadAgentServerInfo was re-throwing all SDK HttpErrors (including 503)
as-is, but root.tsx only checks for AgentServerUnavailableError. A 503
from the static server in --frontend-only mode (or any non-401 error)
would fall through to the Outlet instead of showing the manage-backends
modal.

Narrow the re-throw to only preserve 401 (needed for the auth screen in
public mode). All other HTTP errors are now wrapped as
AgentServerUnavailableError so the app shows the correct recovery UI.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: poll automation readiness in backend-only test; retry on 5xx

The automation backend starts independently from the agent-server and
may not be ready when /server_info first returns 200. pollUrl now treats
5xx responses as 'not ready yet' and keeps retrying. The backend-only
test also polls /api/automation/v1 before asserting on it.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2026-06-03 06:27:25 +00:00
a53bff52d4 fix(chat): polish pending user message sending UX (#1053)
* fix(chat): polish pending user message sending and error UX

Move sending/error status below the bubble, left-align the sending label,
and use theme error colors with a compact retry button. Add hover stop
control on in-flight messages so users can dismiss pending sends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): improve pending send UX and truncate long user messages

Clamp sent user bubbles to three lines with a bottom gradient, pill-style
View more hint (fast fade on hover), and click-to-expand via an overlay
control. Move the in-flight stop control inside the bubble and use the
stop icon.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): refine pending send UX and add dev preview helper

Polish sending/error pending bubbles: adaptive stop button placement, filled stop icon, full-opacity sending state, Thinking-matched status text, and user message spacing. Add dev-only preview seeding via ?previewPendingChat= for mock review.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): restore cancelled sends and stabilize pending bubble layout

Return stopped message text to the input when empty, reserve a fixed stop-button column to prevent text shift, and tighten user bubble vertical padding to balance line-height spacing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): add flowing gradient to sending status text

Reuse the workbench kanban gradient-flow animation on the pending send label for a subtle live-activity shimmer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): polish pending send shimmer, stop control, and preview

Add a reusable TextShimmer for the sending label, overlay the stop button
on the bubble with clearer hover feedback, align sending status spacing with
Thinking, and seed a multiline sending case in the dev preview.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-03 13:16:33 +07:00
1b03dbac43 fix(sidebar): scroll conversation filter menu on short viewports (#1052)
The sidebar filter dropdown had no max height, so on small windows its
Organize/Sort/Show sections extended past the viewport with no way to
reach items at the bottom. Cap height to available viewport space and
use overflow-y-auto with the custom scrollbar only when content overflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-03 12:50:43 +07:00
Hiep Le 09af97be17 fix: stop loading the cloud conversation after switching to a local backend (#1047) 2026-06-03 12:00:23 +07:00
chuckbutkusandopenhands c0c413f406 feat(mcp): render markdown links in helperText; update Slack catalog pin (#1012)
* feat(mcp): render markdown links in helperText; bump extensions to slack field-order PR commit

- Add renderHelperText() to install-server-modal.tsx that converts
  [text](url) patterns into <a> elements with target=_blank, so the
  Slack workspace-ID helper text (and any future catalog entries) can
  embed clickable docs links inline.
- Bump @openhands/extensions to commit 2d43e9c (branch
  slack-catalog-field-order-and-helper-links, PR #285) which:
    • moves SLACK_TEAM_ID before SLACK_BOT_TOKEN in the install modal
    • replaces the plain SLACK_TEAM_ID helper text with linked copy:
      'First visit [here](...#find-your-url) to get your Slack URL
       and then visit [here](...#find-your-workspace-or-org-id) to
       get your workspace ID.'
- Removes stale integrity hash from package-lock.json for the
  @openhands/extensions entry; npm install will recompute it.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to d186872 (SLACK_BOT_TOKEN helperText)

Add inline linked helperText for SLACK_BOT_TOKEN in slack.json (PR #285,
commit d186872): 'You'll need to create or update a Slack App as shown
[here](https://github.com/zencoderai/slack-mcp-server#slack-bot-setup).'
Drops the now-redundant helperLink field.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to b45d3a1 (SLACK_TEAM_ID helperText rewrite)

Update SLACK_TEAM_ID helperText to named links:
'First get your [Slack URL](...). Then use that to get your [Workspace ID](...).'

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 84a0a6e (SLACK_BOT_TOKEN named link)

Update SLACK_BOT_TOKEN helperText to:
"You'll need to create or update a [Slack App](...#slack-bot-setup)."

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to e07f427 (SLACK_BOT_TOKEN helperText)

Update SLACK_BOT_TOKEN helperText to:
"You'll need to create or update a [Slack App](...) to get a Bot token"

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 5efd1b8

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 952c759

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to f30dbfb

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 02715f4

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to cb092c8

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(mcp): validate URL scheme in renderHelperText; use matchAll

- Guard href against javascript:/data: XSS via /^https?:\/\//i test
- Replace exec-in-while with matchAll to drop the eslint-disable comment

Addresses review bot feedback on PR #1012.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(mcp): use double quotes for fallback href to satisfy Prettier

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: update @openhands/extensions to latest main (62594156)

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 22:52:22 +00:00
Graham Neubigandopenhands 667c5162fa [codex] Reapply active LLM profile after save (#961)
* Reapply active LLM profile after save

* chore: add active profile reapply evidence

Simplify the active-profile reapply predicate and cover the recreate-active-profile path from review feedback.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 22:14:45 +00:00
Rohit Malhotraandopenhands 53ed7757cb docs: rewrite self-hosting guide for --public mode (#1038)
* docs: rewrite self-hosting guide for --public mode

Replace the outdated npm-run-dev + nginx-basic-auth workflow with the
production CLI (npx @openhands/agent-canvas --public). Key changes:

- Step 3 now uses a single npx command instead of clone/install/dev
- Security model uses LOCAL_BACKEND_API_KEY + --public (built-in API key
  entry screen) instead of nginx HTTP basic auth as the primary defense
- Removed ~80 lines of htpasswd, POSIX ACL, and apache2-utils setup
- nginx section is now TLS-only (no auth_basic directives)
- Architecture diagram updated: static server replaces Vite dev server,
  LOCAL_BACKEND_API_KEY replaces SESSION_API_KEY
- Section 5 simplified: key comes from LOCAL_BACKEND_API_KEY, no
  session-api-key.txt or cross-origin basic-auth workaround

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: rename "Run the server" to "Run Agent Canvas"

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: add process-persistence guidance (tmux + systemd) to self-hosting guide

Add a brief section after the 'npx --public' command explaining how to
keep the service alive across SSH sessions, covering:
- Option A: tmux one-liner for quick setups
- Option B: minimal systemd unit with Restart=on-failure for long-term deployments

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: use export + openssl rand for key, remove dead anchor link

- Replace inline LOCAL_BACKEND_API_KEY=<secret> pattern with
  export + openssl rand -base64 32 so the key stays out of ps aux
  and shell history
- Add brief explanation of why export is preferred
- Update tmux example to use export before tmux new-session
- Remove dangling reference to non-existent
  #advanced-defense-in-depth section

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 21:53:29 +00:00
Engel Nyst e386ba316c Harden workflow GitHub context handling (#750)
* Harden workflow GitHub context handling

Pass attacker-controllable GitHub context and workflow values through environment variables before shell use.
2026-06-02 22:29:18 +02:00
Hiep Le ce1401d3cd fix(frontend): localize conversation status labels (#1010)
* fix: localize conversation status labels

* refactor: update the code based on feedback

* refactor: update the code based on feedback
2026-06-02 19:11:18 +00:00
Engel Nyst 56545a78c5 Add issue duplicate checker workflow (#987)
* Add issue duplicate checker workflow
2026-06-02 18:54:27 +00:00
Rohit Malhotraandopenhands e408deea9e fix: bind static-server dual-stack to fix Docker E2E ECONNREFUSED on ::1 (#1032)
* fix: bind static-server dual-stack to fix Docker E2E ECONNREFUSED on ::1

The Docker mock-LLM E2E tests were flaky because static-server.mjs
bound to 0.0.0.0 (IPv4 only), but Playwright and Chromium often
resolve `localhost` to ::1 (IPv6) on Ubuntu CI runners, causing
intermittent ECONNREFUSED.

The non-Docker tests didn't have this problem because they go
through ingress.mjs, which calls server.listen(port) without a
host argument — Node.js defaults to :: (dual-stack: IPv4 + IPv6).

Changes:
- static-server.mjs: default host from "0.0.0.0" to null; when
  null, call server.listen(port) without host so Node binds to ::
- docker/entrypoint.sh: drop --host 0.0.0.0 from both
  static-server invocations so they use the dual-stack default
- playwright.mock-llm.config.ts: drop --host 0.0.0.0 from the
  public-mode static server (tests hit it directly via localhost)

Callers behind ingress.mjs (dev-with-automation, dev-static) still
pass --host 0.0.0.0 explicitly, which is fine since the ingress
itself is already dual-stack.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use 127.0.0.1 in Docker E2E config to avoid IPv6 ECONNREFUSED

The Docker mock-LLM E2E tests fail with ECONNREFUSED ::1:18300
because Playwright/Chromium resolve `localhost` to ::1 (IPv6) on
Ubuntu CI runners, but the Docker container's static-server binds to
0.0.0.0 (IPv4 only).

The non-Docker tests don't have this issue because they go through
ingress.mjs which binds to :: (dual-stack: IPv4 + IPv6).

Rather than changing the server binding (which could have
side-effects inside the Docker container), this fix changes the
Docker Playwright config to use 127.0.0.1 directly for all URLs:
INGRESS_URL, MOCK_LLM_BACKEND_URL, MOCK_LLM_PUBLIC_MODE_URL, and
the webServer health-check probe. This bypasses DNS resolution
entirely and connects via IPv4.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: keep Docker container alive when a backend service exits

The Docker entrypoint used `wait -n` which exits the entire
container when ANY child process exits. After heavy automation
tests (which spawn multiple conversations), the agent-server or
automation backend could exit, taking down the static-server with
it — causing ECONNREFUSED for subsequent tests.

In the non-Docker path, each service is an independent host process,
so one crashing doesn't affect the others. The ingress proxy
returns 502 for the dead backend but stays up.

Change the entrypoint to monitor children in a loop: log crashes
but keep the container running as long as any service is still
alive. Only exit when ALL tracked children are dead. The SIGTERM
trap still handles clean shutdown.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: simplify entrypoint keep-alive to avoid wait -n interaction issues

Replace the complex PID monitoring loop with a simple sleep loop.
The previous wait -n based loop regressed automation tests (5/14
vs 8/14 on the simpler IPv4-only commit), likely due to bash
wait -n signal handling interacting poorly with child processes.

The sleep loop keeps the container alive indefinitely. The existing
SIGTERM/SIGINT trap handles clean shutdown.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review feedback on entrypoint and docs

- Wait on STATIC_PID only (not all children): the static-server is
  the critical ingress process. If it dies the container exits with
  a meaningful exit code. Backend crashes (agent-server, automation)
  are tolerated — the proxy returns 502.  (Copilot feedback)

- Add `exit 0` to cleanup(): ensures the script terminates after a
  SIGTERM-triggered trap return instead of falling through. The
  `wait` builtin is signal-interruptible so SIGTERM is processed
  immediately — no stale sleep blocking delivery.  (all-hands-bot)

- Update AGENTS.md to match the actual behavior (wait on static-server
  PID, not a monitoring loop or infinite sleep).  (Copilot feedback)

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use signal-safe sleep-wait loop with static-server liveness check

The bare `wait "$STATIC_PID"` approach failed the same way as
the original `wait -n` (8/14 — conversation/model-switch tests
get ECONNREFUSED after automation).  The `while true; do sleep
86400; done` pattern from the previous commit was the only one
that passed 14/14, but had two issues flagged in review:

1. Bare `sleep` as foreground blocks SIGTERM delivery (all-hands-bot)
2. Container stays alive forever even if static-server dies (Copilot)

This commit addresses both:
- `sleep 10 & wait $!` — `wait` (builtin) is the foreground op,
  so SIGTERM interrupts it immediately and the trap fires.
- `while kill -0 "$STATIC_PID"` — loop exits when the critical
  ingress process dies; container exits with a meaningful code.
- `cleanup()` keeps `exit 0` so the script terminates after a
  signal-triggered trap return.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 18:33:39 +00:00
dbc3abfed3 docs: clarify README install option requirements (#945)
* Clarify README install option requirements
* docs: add Windows PowerShell syntax for Docker sandbox option

Split Option 2 into Linux/Mac and Windows (PowerShell) code blocks to address
cross-platform shell syntax differences. The original Unix-style export syntax
doesn't work on Windows CMD or PowerShell.

* docs: refine install commands across platforms
* docs: move Windows PowerShell instructions to separate README

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@openhands.dev>
Co-authored-by: enyst <engel.nyst@gmail.com>
2026-06-02 20:01:25 +02:00
8b3ebbedba App 2052/optionally sync mcp config with secrets (#1001)
* add toggles to form fields

* refactor(mcp-secrets): improve save-as-secret hook, toggle a11y, and add tests

- useSaveFieldsAsSecrets: wrap returned fn in useCallback for stable identity,
  pass field.label as secret description, truncate key lists >3 in toasts,
  update JSDoc to document upsert behaviour
- SaveAsSecretToggle: replace hidden input with sr-only for AT accessibility,
  move data-testid to label, add aria-hidden to decorative track, replace
  div with button for keyboard-reachable info trigger with aria-label
- InstallServerModal: add stateRef to avoid stale closure in onSuccess
  callback, add comment explaining why argFields have no secret toggle
- translation.json: plural-safe English wording for MCP$SECRETS_SAVED
- tests: add use-save-fields-as-secrets.test.ts (9 tests), add
  save-as-secret-toggle.test.tsx (9 tests), extend install-server-modal
  with save-as-secret describe block (8 tests); 35/35 pass

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: update the code based on feedback

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-03 00:46:16 +07:00
936b950dc0 Design updates: navigation polish, modal tokens, and git control bar fixes (#884)
* Fix ACP provider registry typing for the pinned typescript-client.

Assert model list fields on the upstream registry record so staged typecheck
passes until the client export catches up.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Centralize form-control transitions and chat input pill button classes.

Extract shared transition tokens and chatInputPill/Icon class names so hover
timing and styling stay consistent across the chat input action row.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add shared dropdown classes for instant hover colors.

Introduce dropdown-classes utilities so menus and combobox rows can disable
transition-colors delays from one place.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply instant hover colors across dropdown and context menu rows.

Replace transition-colors on menu triggers and rows with shared dropdown
classes so text and background colors snap on hover.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add collapsible automation prompt section with gradient fade.

Long prompts default to a max height with a bottom dissolve and View More toggle
so the detail page stays scannable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add play icon to the automation detail Run now button.

Match the grid/list Run now affordance and update the detail test to query the
i18n key used in mock mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix add automation modal title color on dark surfaces.

Apply text-white to the heading so it remains readable on bg-base-secondary.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix switch profile menu row alignment for two-line profiles.

Override items-center from shared menu row classes with items-stretch so
flex-col profile rows stay left-aligned.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Standardize dropdown list spacing and tighten profile menu header padding.

Add shared 2px row gaps across context menus and dropdown panels, and remove stacked top padding above the switch-profile "Available Profiles" heading.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Unify dropdown row icon styling, spacing, and alignment.

Use shared muted-to-white icon hover tokens, a fixed 16px icon slot for label alignment, and standard row gaps across combobox menus, context menus, and the backend selector footer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use secondary cancel styling on the LLM profile editor footer.

Remove the top border divider so the action row matches the rest of the form layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Extract a shared BackNavButton for settings and detail back links.

Centralizes muted back navigation styling so automations, LLM profiles, and secrets use the same control.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Cap switch-profile menu scrolling at four profile rows.

Move overflow off the outer context menu so the header and settings link stay
pinned while only the profile list scrolls. Max height is sized for four
two-line rows (name + model) via switchProfileMenuListScrollClassName.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Show active mode with a checkmark in the change-agent menu.

Pass conversationMode into the context menu and reuse the shared menu row
checkmark pattern for Code vs Plan selection.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use a blue hover fill on the active Plan agent pill.

Replace the grey white/10 overlay with the plan accent border color so hover
stays in the Plan palette.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add themed modal title tokens and shared class helpers.

Introduce --oh-modal-title-foreground plus modalTitle*ClassName helpers so
modal headings can stay white on dark surfaces without hardcoded text-white.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Route BaseModalTitle through the shared modal title token.

Default modal headings now use modalTitleClassName instead of text-content-2,
and compact workspace modals use modalTitleSmClassName rather than ad hoc white.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply modal title tokens to backend management dialogs.

Use modalTitleLgClassName and modalTitleLgMediumClassName for add, edit, and
manage backend modal headings.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply modal title tokens to skills dialogs.

Use modalTitleLgClassName for skill detail and add-skill modal headings.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply modal title tokens to automation dialogs.

Replace hardcoded text-white headings with modalTitleLg*ClassName helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply modal title tokens to MCP install and editor dialogs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply modal title token to the legacy AI settings modal.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore white plugin launch modal title styling.

Drop the text-content-2 override so Typography.H2 uses its default white heading.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix git control bar inactive styling during conversation loading.

Wait for history preload before enabling Pull/Push/PR actions and use shared muted theme tokens for the disabled state.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove setup-time pills from recommended automation cards.

Drop the clock duration tag so recommended automations only show MCP and connect-state metadata.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Polish extensions nav label and modal title weight.

Rename the extensions sidebar heading to Customize: and use medium weight for large modal titles.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-02 23:59:55 +07:00
Rohit Malhotraandopenhands 3c52a10d4b test: add mock-LLM E2E test for /model slash command (mid-conversation profile switch) (#998)
* test: add mock-LLM E2E test for /model slash command

Add tests/e2e/mock-llm/mock-llm-model-switch.spec.ts exercising the
full /model mid-conversation profile switch flow against the real
agent-server with a scripted mock LLM backend.

Step 1 — setup:
  - ensureMockLLMProfile() configures agent_settings.llm (proven pattern)
  - POST /api/profiles/{name} creates profile B as the switch target
  - Registers a 3-entry trajectory: padding for the internal LLM call,
    INITIAL_REPLY_TOKEN, POST_SWITCH_REPLY_TOKEN

Step 2 — conversation + /model switch:
  - Starts conversation from home page, waits for agent reply
  - Types '/model model-switch-profile-b' and submits
  - Verifies 'Switched to profile' confirmation in data-testid=model-messages
  - Verifies POST /api/conversations/{id}/switch_profile was intercepted
  - Sends follow-up, verifies agent responds (post-switch continuity)
  - Verifies no error banners

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(ci): prevent main-branch Docker E2E from posting comments to PRs

When the Docker workflow completes on main, it triggers
mock-llm-docker-e2e.yml via workflow_run. GitHub's API can populate
workflow_run.pull_requests[] with unrelated open PRs (stale association
from shared commit ancestry). This caused main-branch Docker E2E
failures to post ❌ comments on random PRs.

Fix: skip PR number extraction when the triggering workflow ran on
main/master. Main-branch runs still execute (validating the published
image) but no longer post misleading comments to PRs. PR-specific runs
via the pull_request trigger are unaffected.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: address review comments on mock-LLM model-switch test

- Import APIRequestContext from top-level @playwright/test import
  instead of inline import() type references (review #1)
- Extract setChatInput to shared mock-llm-helpers.ts and reuse it
  in both mock-llm-conversation and mock-llm-model-switch (review #2)
- Add upstream reference for the padding trajectory entry coupling
  to CondensationMixin (review #3)
- Use direct field assertion on switchProfileBody.profile_name
  instead of loose JSON.stringify substring match (review #4)
- Remove redundant toBeVisible check after waitForNonUserMessageText
  already polls model-messages elements (review #5)

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(ci): deduplicate Docker E2E workflow_run triggers + add retry

Two Docker E2E reliability fixes:

1. Concurrency group: workflow_run triggers used the unique
   workflow_run.id as the group key, so multiple Docker builds
   completing on main each spawned their own E2E run (they never
   cancelled each other). Changed to key by the triggering
   workflow's head_branch, so main-branch workflow_run triggers
   share the group 'wr-main' and only the latest run survives.
   pull_request triggers still key by PR number (unchanged).

2. Retry: Docker Playwright config now uses retries:1 in CI to
   handle transient container startup ECONNREFUSED failures. The
   webServer health-check confirms the stack is up, but occasional
   races between container readiness and the first test request
   can still cause failures.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: address second round of review comments

- Make saveProfile idempotent with best-effort delete-first so stale
  profiles from crashed CI runs don't cause persistent 409 failures
- Increase switch-confirmation timeout from 15s to 30s for consistency
  with all other waitForNonUserMessageText calls in this test
- Add waitForTestId guard before post-switch setChatInput to handle
  potential UI disable during profile switch settling

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: minor nits — non-null assertion + testId in error message

- Use switchProfileBody!.profile_name after toBeTruthy guard instead
  of optional chain (the assertion guarantees non-null)
- Include testId in setChatInput error message for easier diagnosis;
  accept optional testId parameter for future reuse

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 16:18:52 +00:00
Tim O'Farrellandopenhands 3e51a68bd4 chore: auto-graduate npm dist-tag from latest to per-tier once first stable release ships (#1028)
* chore: always publish to npm with --tag latest until first stable release

All alpha/beta/rc versions now get the 'latest' dist-tag so plain
'npm install @openhands/agent-canvas' always resolves to the newest
published release. The per-tier dist-tags (alpha/beta/rc) can be
re-introduced once the first full stable version is ready to ship.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: auto-graduate npm dist-tag when first stable release ships

At publish time, query npm for any published version without a pre-release
suffix. If none exists, all releases (alpha/beta/rc/stable) use --tag latest
so plain 'npm install' always resolves to the newest build. Once a stable
version has been published, pre-release versions revert to their own
dist-tags (alpha/beta/rc) automatically — no workflow change required.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 10:07:54 -06:00
Tim O'Farrellandopenhands 5a3011c001 fix: align dev-stack OH_PERSISTENCE_DIR and automation DB path with Docker (#960)
* fix: align dev-stack OH_PERSISTENCE_DIR and automation DB path with Docker

Two path mismatches between `npm run dev` and Docker prevented config and
automation data from being shared across the two modes:

1. **OH_PERSISTENCE_DIR wrong (supersedes PR #959)**
   Docker's entrypoint.sh sets OH_PERSISTENCE_DIR to $HOME/.openhands
   (OPENHANDS_DIR). PR #959 added the env var to buildAgentServerEnv() but
   used config.stateDir (~/.openhands/agent-canvas) — one level too deep.
   Settings and secrets written by Docker live at ~/.openhands/settings.toml
   etc; dev wrote to ~/.openhands/agent-canvas/settings.toml.
   Fix: use path.dirname(config.stateDir) = ~/.openhands, matching Docker.

2. **Automation DB in wrong directory**
   dev-with-automation.mjs put the SQLite DB at
   ~/.openhands/agent-canvas/automations.db. Docker puts it at
   ~/.openhands/automation/automations.db (from config/defaults.json
   paths.automationDb = "automation/automations.db" relative to OPENHANDS_DIR).
   Fix: use join(dirname(config.stateDir), SHARED_DEFAULTS.paths.automationDb)
   so both modes resolve to ~/.openhands/automation/automations.db.
   Also ensure the directory is created on startup (mirrors Docker's mkdir -p).

3. **Mock-LLM test cleanup**
   Update playwright.mock-llm.config.ts to clean both STATE_DIR and the new
   AUTOMATION_DB_DIR (.tmp/automation/) before each test run, since the DB now
   lives outside STATE_DIR.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use dev_conversations dir for npm to isolate from Docker conversations

npm dev mode now writes conversations to ~/.openhands/agent-canvas/dev_conversations
instead of conversations, keeping them separate from Docker's conversations dir.

OH_CONVERSATIONS_PATH (set via buildAgentServerEnv) is the single control point;
all mkdir and releaseStaleConversationLeases calls updated to match.

Also condense verbose multi-line comments on OH_PERSISTENCE_DIR and
AUTOMATION_DB_URL to single lines.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use join(config.stateDir, dev_conversations) in ensureDirectories

The outer config in dev-with-automation.mjs does not have conversationsPath
(that is a SafeDevConfig property). Use the same join(stateDir, ...) pattern
as the other dirs in the list.

Also removes the debug console.log and restores dev-safe.mjs and dev-static.mjs
to dev_conversations after the manual revert.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: update conversationsPath assertion to match dev_conversations

The implementation in buildConfigFromPorts was changed to use
'dev_conversations' as the subdirectory name, but the corresponding
test assertion was not updated.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 15:26:19 +00:00
Tim O'Farrellandopenhands e5907df5b4 ci: trigger CI on rel-* branch pushes for tag protection rule (#1004)
The Release Tag ruleset requires test-and-build (ubuntu) to pass
before v* tags can be pushed, but CI previously only ran on main and
pull_request events. This caused rel-* version bump commits to fail
the tag protection check unless a workaround PR was opened.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 14:57:44 +00:00
Hiep Le 51c22c22c4 fix: stop sending agent_context: null in cloud settings saves (#1021) 2026-06-02 12:36:28 +00:00
simonrosenbergandDebug Agent 779cba5f6e docs(acp): add a guide for using ACP agents (Claude Code, Codex, Gemini CLI) (#972)
* docs(acp): add ACP agents onboarding/config guide

A guide for onboarding and configuring external ACP agents (Claude Code, Codex,
Gemini CLI): where each stores its login, which credentials onboarding collects,
and how subscription/login vs API-key precedence works.

The credentials-collection code originally in this PR landed via #1002
(registry-derived fields including Gemini), so #972 is now docs-only — rebased
onto current main.

* docs(acp): state that a subscription login takes priority over API keys

Make explicit that a subscription / OAuth login is preferred over an env API
key for all three providers — while signed in, a key isn't used (the onboarding
fields do nothing). Corrects the earlier claim that Claude Code uses
ANTHROPIC_API_KEY instead of the login: with both present, `claude auth status`
reports it's using the subscription (claude.ai). The base URL remains the one
override that takes effect under a login.

* docs(acp): show the conversation-turns edge in the flow diagram

Canvas relays each turn through the Agent Server, not just the settings PATCH;
add that edge so the diagram doesn't read as config-only (addresses review).

---------

Co-authored-by: Debug Agent <debug@example.com>
2026-06-02 12:25:30 +00:00
Hiep Le 6c376bdd57 feat(frontend): warn when the LLM is not configured after skipping onboarding (#1011)
* feat: warn when the LLM is not configured after skipping onboarding

* refactor: update the code based on feedback
2026-06-02 16:38:39 +07:00
5a60043fcd feat(onboarding): client-side ACP login detection + collect Gemini credentials (no SDK changes) (#1002)
* feat(onboarding): detect ACP login client-side via the bash endpoint

Auto-detect whether the chosen ACP provider (Claude Code / Codex / Gemini) is
already signed in and show a "✓ you're already signed in" banner instead of
unconditionally asking for an API key. Refs OpenHands/agent-canvas#964.

This is a 100% canvas implementation — no SDK endpoint, no SDK release, no
version pin. Detection is gated to local backends (where the provider CLIs and
credential files actually live) and runs the provider's own status command
through the existing agent-server bash endpoint, classifying the output:

- Claude Code → `claude auth status --json` (read `loggedIn`; exits non-zero
  when logged out, so we read the JSON, not the exit code).
- Codex → `codex login status` ("Logged in …" / "Not logged in" — on stderr,
  so both streams are checked).
- Gemini → has no status command (browser OAuth), so check its credentials
  file `~/.gemini/oauth_creds.json`.

Anything that can't be classified — CLI not installed ("command not found"),
unexpected output, or the bash call failing — is reported as `unknown`, so
onboarding falls back to the API-key fields rather than a false banner. Verified
live end-to-end through the Vite proxy → bash endpoint: all three authenticated
on a logged-in machine; a CLI stripped from PATH (exit 127) → `unknown`.

- `AcpService.getAuthStatus(server)` (`BashClient`-based) returns the classified
  status; `useAcpAuthStatus` is gated to local backends and no longer keys off
  whether the provider has API-key fields, so Gemini is detected too.
- The credentials step renders a login-status screen (banner, no inputs) for
  key-less providers like Gemini; the onboarding flow no longer skips slide 2.
- New i18n: ACP_AUTH_DETECTED, ACP_AUTH_CHECKING, ACP_LOGIN_TITLE,
  ACP_LOGIN_SUBTITLE, ACP_AUTH_DETECTED_NO_KEY.

Tests: `acp-service.api.test.ts` covers each provider's classifier incl. the
missing-CLI → `unknown` path (the "no available ACP process" case); hook/step/
modal tests cover gating, the Gemini login-only screen, and no-skip routing.

Supersedes the protocol-probe approach (software-agent-sdk#3452 /
typescript-client#196 / the #971 wiring), which can be closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: address PR review feedback (#1002)

- Move acp-service.api.test.ts under __tests__/ for consistency with the
  project's test layout.
- classifyGemini: exact (trimmed) match on present/absent instead of substring,
  so stray output falls through to "unknown".
- Document why `retry: false` co-exists with probeAcpAuth's internal catch.

* chore: fix stale GET /api/acp/auth-status comment (#1002)

The endpoint design was replaced by bash-command detection via AcpService;
update the comment in setup-acp-secrets-step to match.

* chore: address PR review feedback (#1002)

- classifyGemini reads only stdout (the echo writes there); avoids a stray
  stderr line turning a real result into unknown.
- Shorten probe timeout 30s -> 10s; the detection commands are fast local
  checks, so a long onboarding spinner isn't warranted.

* feat(onboarding): collect ACP credentials for all providers incl. Gemini

Onboarding's ACP credentials step now offers an API-key (+ optional base-URL)
field for Gemini too, not just Claude Code and Codex. Refs agent-canvas#972.

- getAcpProviderSecrets now derives fields from the SDK registry's
  api_key_env_var / base_url_env_var (via @openhands/typescript-client) instead
  of a hand-maintained per-provider map — so all three built-ins (Claude Code,
  Codex, Gemini CLI) get their fields and the list tracks the SDK with no
  parallel copy to drift.
- The step renders the standard key fields for every ACP provider plus a note
  that a subscription / OAuth login (Gemini's Google login, a Claude login)
  takes precedence, so the keys are optional. The detection banner still shows
  "you're already signed in" on top when a login is found.
- Drops the now-unreachable Gemini-only "login screen" branch and its three
  unused i18n keys (ACP_LOGIN_TITLE/SUBTITLE, ACP_AUTH_DETECTED_NO_KEY); adds
  ACP_SECRETS_SUBSCRIPTION_NOTE.

Tests updated: Gemini now asserts a GEMINI_API_KEY field renders alongside the
banner (step + modal).

* docs: drop transient-branch-state comments

Remove comments that narrated this (unreleased) branch's evolution rather than
describing the code — e.g. 'the flow no longer skips it', 'now exposes',
'login-status screen (no key fields)'. The modal slide-2 comment now just states
current behavior (every ACP provider shows the credentials form).

* fix(onboarding): clearer "already signed in" banner copy

The banner said "adding an API key below is optional", which read ambiguously.
Reword to "you can leave the fields below blank" — the subscription/login takes
precedence over an env API key for all three providers (verified: Claude reports
authMethod=claude.ai even with ANTHROPIC_API_KEY set; Codex stays on ChatGPT;
Gemini uses its OAuth auth-type), so the key isn't needed while signed in.

Also hide the redundant "leave these blank" subtitle note once authenticated —
the banner already conveys it.

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:18:16 +00:00
Hiep Le e6a39f357f fix(frontend): show clear preview unavailable message for office documents (#958)
* fix: show clear preview unavailable message for office documents

* refactor: update the code based on feedback

* refactor: update the code based on feedback
2026-06-02 11:45:48 +07:00
Tim O'Farrellandopenhands d0994a6fe1 fix: use X-Session-API-Key for local automation auth in prompts and RUNTIME_SERVICES (#999)
Fixes #980

The agent prompt in recommended-automations-launcher and the
RUNTIME_SERVICES block in agent-server-adapter both advertised
X-API-Key as the auth header for the local automation backend.
The automation service (openhands-automation) does not accept
X-API-Key — it accepts Authorization: Bearer and X-Session-API-Key.

X-Session-API-Key is the established local convention: the agent
server uses it, the frontend automation API client uses it (with an
explicit comment that both backends share the same header), and
auth.py describes it as matching that convention. Update both call
sites and the corresponding test assertion to use X-Session-API-Key.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 15:10:30 -06:00
Rohit Malhotraandopenhands 449d1fc9e5 feat: reuse mock-LLM E2E tests for Docker image validation (#992)
* feat: reuse mock-LLM E2E tests for Docker image validation

Add a Docker-specific Playwright config (playwright.mock-llm-docker.config.ts)
that runs the exact same test specs and helpers against the agent-canvas Docker
image instead of the npm build path (bin/agent-canvas.mjs + uvx).

Key changes:

- Split MOCK_LLM_BASE_URL into two constants in mock-llm-helpers.ts:
  - MOCK_LLM_BASE_URL: always host-local, used by tests for admin API
  - MOCK_LLM_AGENT_URL: env-overridable, used when configuring the LLM
    profile (the URL the agent-server uses for inference). Defaults to
    MOCK_LLM_BASE_URL for backward compatibility with the npm path.

- New playwright.mock-llm-docker.config.ts:
  - Starts the mock LLM server on the host (same as npm path)
  - Runs the Docker container with --network host (Linux CI)
  - Points to the same testDir (tests/e2e/mock-llm/) and specs
  - Separate output dirs to avoid collision with npm path results

- New CI workflow (.github/workflows/mock-llm-docker-e2e.yml):
  - Builds the Docker image from current code (or uses a pre-built image)
  - Runs the same specs against the container
  - Posts PR comment with differentiated report title

- render-mock-llm-report.mjs: accept --title flag for Docker vs npm reports
- npm run test:e2e:mock-llm:docker script added
- .gitignore updated for docker test output dirs

The npm path (test:e2e:mock-llm) is fully backward-compatible — no env var
override needed since MOCK_LLM_AGENT_URL defaults to MOCK_LLM_BASE_URL.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: chain Docker E2E off existing Docker CI via workflow_run

Instead of rebuilding the Docker image in the E2E workflow (duplicating
~10-15 min of Docker build time), use workflow_run to trigger automatically
after the existing 'Docker' workflow completes successfully.

The workflow now:
- Triggers on: workflow_run (Docker completed) + workflow_dispatch (manual)
- Derives the image tag from the Docker build's commit SHA
  (ghcr.io/openhands/agent-canvas:sha-<short>-amd64)
- Pulls the already-built image from GHCR — no rebuild needed
- Checks out code at the same SHA as the Docker build
- Extracts PR number from workflow_run.pull_requests[] for comments

Removed: Docker build steps, Buildx setup, build-arg resolution.
All image building stays in docker.yml where it belongs.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: replace flaky 1s timeout with polling for Active badge assertion

The 'Active badge' check in step 2 used a hardcoded 1-second
waitForTimeout before reloading. On a loaded CI runner the profile
activation mutation may not persist in time, causing the reload to
show stale state. This is a pre-existing flake (identical test code
passed on the first push and failed on the second).

Replace with expect.poll() that retries the reload+check cycle with
increasing intervals (1s, 2s, 3s) up to 15 seconds total.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add pull_request trigger for Docker E2E (workflow_run bootstrap)

workflow_run only fires when the workflow file exists on the default
branch (main). Since mock-llm-docker-e2e.yml is new and only on the
PR branch, GitHub doesn't recognize it as a workflow_run listener yet.

Add pull_request trigger (gated by 'e2e-tests' label, skip forks) that
polls the Docker workflow via gh API until it completes for the PR's
head SHA, then pulls the already-built image from GHCR and runs tests.

After merge, workflow_run takes over as the primary automatic trigger.
The pull_request path remains as a fallback for label-gated runs.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add FILE_STORE, AUTOMATION_BASE_URL, AUTOMATION_WORKSPACE_BASE to Docker entrypoint

The Docker entrypoint was missing several environment variables that the npm
path (dev-with-automation.mjs) sets for the automation backend:

- FILE_STORE=local — without this, the automation backend may fall back to
  cloud storage (S3/GCS) which fails without credentials, causing tarball-
  based presets (preset/prompt, preset/plugin) to silently error
- LOCAL_STORAGE_PATH — where to store files on the local filesystem
- AUTOMATION_BASE_URL — publicly-reachable base URL for callback URLs
- AUTOMATION_WORKSPACE_BASE — where automation runs unpack tarballs

This explains the Docker E2E failure: the agent's curl to create an automation
via /api/automation/v1/preset/prompt returned an error (likely 500 from missing
storage config), but the mock LLM doesn't care about terminal output and
proceeded to return the scripted final reply. The test then found 0 automations.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: exclude auth-modes spec from Docker E2E tests

The mock-llm-auth-modes.spec.ts tests npm-binary-specific --auth-required
behaviour (a second static-server instance on port 18301). The Docker image
doesn't provide this second server — it has its own auth handling. Exclude
the spec from the Docker test run via testIgnore.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: run auth-modes tests inside Docker via PUBLIC_MODE_PORT

Instead of excluding the auth-modes spec from the Docker E2E run or
spinning up a host-side static server with a duplicate build/ directory,
the Docker entrypoint now supports an optional PUBLIC_MODE_PORT env var.

When set, entrypoint.sh starts a second static-server instance from the
same baked-in frontend assets with --auth-required (no session key
injected). This tests the actual Docker image's auth gate behaviour —
not a host-side approximation.

The Playwright Docker config passes -e PUBLIC_MODE_PORT=18301 to the
container and exports MOCK_LLM_PUBLIC_MODE_URL so the auth-modes spec
can reach it. With --network host the port is accessible from the host.

Co-authored-by: openhands <openhands@all-hands.dev>

* address review feedback: drop unlabeled trigger, improve error messages, document env vars

- Drop 'unlabeled' from pull_request trigger types to avoid wasted
  workflow runs when any label is removed (the job-level if: condition
  would skip immediately anyway)
- Distinguish 'no Docker run found' vs 'didn't complete in time' in
  the polling loop's final error message
- Add comment explaining /api/automation/v1 probe returns 200 without
  auth so the readiness check won't spin for 180s
- Document FILE_STORE, LOCAL_STORAGE_PATH, AUTOMATION_BASE_URL, and
  AUTOMATION_WORKSPACE_BASE in the entrypoint header — these affect
  production deployments, not just E2E tests

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 15:09:01 -04:00
Tim O'Farrellandopenhands 1d4fd1525c chore: switch to tag-based release workflow with per-tier npm dist-tags (#996)
- create-release.yml now triggers on v* tag push (not PR merge).
  The release branch is never merged to main; publishing is triggered
  by pushing the tag directly to the rel-X.Y.Z branch.
- npm-publish.yml resolves the dist-tag from the version string:
    alpha → alpha, beta → beta, rc → rc, stable → latest
  Removes the temporary 'always publish as latest' workaround.
- release.md skill and AGENTS.md updated to document the new model.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 12:44:05 -06:00
14b1b1e8ad feat: two auth modes — local (auto-key) and public (paste-key) (#790)
* feat: two auth modes — local (auto-key) and public (paste-key)

Local mode (agent-canvas, no flags):
- Ingress binds to 127.0.0.1 only
- Auto-generates session API key
- Writes /backends.json to static dir so frontend auto-authenticates
- Zero setup for localhost use

Public mode (agent-canvas --public):
- Ingress binds to 0.0.0.0 (all interfaces)
- Requires LOCAL_BACKEND_API_KEY env var
- Does NOT write /backends.json
- Frontend shows API key entry screen on 401

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: reuse BackendForm in public-mode API key entry screen

Replace the bespoke ApiKeyEntryScreen form with BackendForm configured
for the public-auth use case:

- Host field is auto-filled from window.location.origin and read-only
- Name field is hidden (auto-derived from the existing backend)
- Only the API key input is exposed to the user
- Uses the same SettingsInput / BrandButton components as the backend
  connection modals for visual consistency

BackendForm gains three optional props to support this:
- hideName: hides the name input and uses a fallback name
- hostReadOnly: disables the host input
- onSubmitPayload: receives the submitted payload for side-effects
  (the API key screen uses it to persist to agent-server-config and
  reload the page)

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md with ApiKeyEntryScreen BackendForm reuse details

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: implement public mode auth flow (--public flag)

- Add --public flag to dev-with-automation.mjs and bin/agent-canvas.mjs
- In public mode: require LOCAL_BACKEND_API_KEY, use as session key,
  don't bake into frontend (no VITE_SESSION_API_KEY / --session-api-key)
- Add isAgentServerAuthError() to detect 401 from /server_info probe
- root.tsx shows ApiKeyEntryScreen when 401 detected (lazy loaded)
- useConfig skips retries on 401 for instant auth screen display
- ApiKeyEntryScreen now has default export for React.lazy compatibility

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use VITE_AUTH_REQUIRED flag instead of 401 detection for public mode

The 401-based approach was unreliable — /server_info may not require
auth on all server versions. Instead:

- dev-with-automation.mjs sets VITE_AUTH_REQUIRED=true in public mode
- isAuthRequiredAndMissing() checks the flag + localStorage for a key
- root.tsx gates on the flag BEFORE the /server_info probe, so the
  auth screen appears instantly with zero network round-trips
- 401 fallback kept as safety net for edge cases

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: handle stale key via 401 detection in public mode

When the server restarts with a new LOCAL_BACKEND_API_KEY, the browser
still has the old key in localStorage. isAuthRequiredAndMissing() returns
false (key exists), so the /server_info probe fires and 401s.

isAgentServerAuthError() now checks VITE_AUTH_REQUIRED=true AND 401
status, so it only triggers in public mode (a 401 in local mode is a
misconfiguration, not a key-rotation event). useConfig skips retries
on 401 to show the auth screen immediately.

Two gates, one screen:
- No key at all → flag check, instant, no network
- Stale key → /server_info 401, one round-trip

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: validate stale keys against GET /api/settings (protected)

/server_info is unprotected — it returns 200 even with a wrong key.
In public mode, after the /server_info probe succeeds, we now hit
GET /api/settings to verify the stored key is still valid. A 401
from that endpoint triggers the auth screen via isAgentServerAuthError().

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: rewrite ApiKeyEntryScreen — validate before save, always empty key, match add-modal UI

Three fixes:

1. Stale key conflict: The form now always starts with an empty API key
   field instead of pre-filling from the backend registry. Stale
   credentials from a previous session never bleed into the input.

2. Wrong key indicator: On submit, the key is validated against
   GET /api/settings (protected endpoint) BEFORE persisting. Wrong
   keys show an inline red status dot + 'Invalid API key' error
   via BackendStatusDot. Only validated keys trigger the reload.

3. UI parity with add-backend modal: Replaced BackendForm wrapper
   with direct SettingsInput fields matching ManualConnectionColumn's
   layout — host (read-only + helper text), API key (password with
   placeholder), status indicator, and Connect button. No cloud
   OAuth column.

New i18n key: AUTH$INVALID_KEY (all 15 languages).

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: match add-backend modal UI + add test coverage

ApiKeyEntryScreen now renders the exact same card chrome as
BackendFormModal add-mode: same title ('Add a Backend'), same
Name/Host/API Key fields, same Connect button styling. Host is
pre-filled and read-only; no cloud OAuth column.

New tests (12 total):
- api-key-entry-screen.test.tsx (7 tests):
  - UI field parity with add-backend modal
  - Stale key wipe (empty API key field despite stale localStorage)
  - Connect disabled until name + key filled
  - Valid key: validates → persists → reloads
  - Invalid key: error indicator, no persist, no reload
  - Retry flow: wrong key → error → correct key → success
  - Stale key isolation: only fresh key persisted
- agent-server-config.test.ts (5 new tests for isAuthRequiredAndMissing):
  - Flag unset → false
  - Flag set, no key → true
  - Flag set, localStorage key → false
  - Flag set, VITE_SESSION_API_KEY → false
  - Flag not 'true' → false

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: distinguish 401 from other errors in ApiKeyEntryScreen

The catch-all was showing 'Invalid API key' for EVERY failure —
including 500s, network errors, and timeouts — even when the key
was correct. Now:

- 401 → 'Invalid API key. Please check the key and try again.'
- Anything else → 'Connection failed: <actual error message>'

This reveals the real problem when a correct key fails for a
non-auth reason (e.g. server misconfiguration, missing OH_SECRET_KEY).

New i18n key: AUTH$CONNECTION_FAILED (all 15 languages).
New test: non-401 errors show 'Connection failed' + detail.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: agent-server receives wrong session key in public mode

startAgentServer() called buildSafeDevConfig() which generated its
own random session key, ignoring config.sessionApiKey (which holds
LOCAL_BACKEND_API_KEY in public mode). The agent-server was started
with a random key while users were told to paste the LOCAL_BACKEND_API_KEY
value — every key was rejected with 401.

Fix: override OH_SESSION_API_KEYS_0 in the agent-server env with
config.sessionApiKey so both the agent-server and the frontend
agree on which key is valid.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: address review comments on ApiKeyEntryScreen

1. Remove dead BackendFormProps (hideName, hostReadOnly, onSubmitPayload)
   — ApiKeyEntryScreen is standalone so no caller used these props.

2. Auto-generate backend name from window.location.hostname instead of
   requiring users to type one. Only the API key field is required now,
   reducing public-mode auth to a single-field flow.

3. Simplify redundant ternary: connectionStatus === 'success' ? true : false
   → connectionStatus === 'success'.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address second round of review comments

1. Use shared isSdkHttpError() helper in ApiKeyEntryScreen instead of
   duplicating the SDK error shape check inline. Exported the helper
   from agent-server-compatibility.ts.

2. Add code comment acknowledging the edge case where a network hiccup
   between /server_info and getSettings() probes lets the app load with
   an unvalidated key. Acceptable since the window is narrow and a page
   refresh recovers.

3. Add --auth-required flag to static-server.mjs so pre-built static
   binaries (npx @openhands/agent-canvas --public) show the API key
   entry screen without needing VITE_AUTH_REQUIRED baked in at build
   time. The flag injects window.__AGENT_CANVAS_AUTH_REQUIRED__=true
   into index.html at runtime. Frontend isAuthRequired() checks both
   the build-time env var and the runtime window flag.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use double cast (unknown) to satisfy strict TS on window flag access

window cannot be cast directly to Record<string, unknown> — TypeScript
requires going through unknown first for unrelated types.

  (window as unknown as Record<string, unknown>).__AGENT_CANVAS_AUTH_REQUIRED__

This fixes the CI typecheck failure introduced in aa76c01a.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address remaining review comments on auth modes PR

- Use isAuthRequired() instead of raw import.meta.env.VITE_AUTH_REQUIRED
  in isAgentServerAuthError() so the runtime window flag injected by
  static-server.mjs in pre-built binaries is also honoured (bug fix).

- Preserve existing backend name during re-authentication flow in
  ApiKeyEntryScreen; only fall back to window.location.hostname for
  the initial entry so users don't lose custom labels on key rotation.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: restore MCP-to-integrations migration from main

A prior merge into this branch incorrectly kept the old
@openhands/extensions/mcps imports instead of the
@openhands/extensions/integrations paths introduced by d41bfe15
on main. Restore all affected files from origin/main so the
extensions package (which no longer exports ./mcps) resolves
correctly.

Files restored from main:
- src/utils/mcp-marketplace-utils.ts
- src/routes/mcp.tsx
- src/components/features/mcp-logo-badge.tsx
- src/components/features/mcp-page/* (6 files)
- src/components/features/automations/* (2 files)
- __tests__/ (4 test files)

Co-authored-by: openhands <openhands@all-hands.dev>

* style: fix prettier formatting and remove unused eslint-disable in api-key-entry-screen

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address remaining PR review comments

- Extract isSdkHttpStatusError() helper in agent-server-compatibility.ts
  to DRY up the SDK error status check (review comment #3321202503).
  Both isAgentServerAuthError() and ApiKeyEntryScreen now use it.

- Use AUTH i18n keys in api-key-entry-screen.tsx:
  • Heading: AUTH$API_KEY_REQUIRED_TITLE ('API Key Required')
  • Description: AUTH$API_KEY_REQUIRED_DESCRIPTION added below heading
  • Button: AUTH$CONNECT ('Connect')
  (review comments #3325478721, #3325478729)

- Fix nested <main> landmark in root.tsx: remove the outer <main>
  wrapper since ApiKeyEntryScreen already provides its own semantic
  container (review comment #3325478704).

- Change ApiKeyEntryScreen root element from <main> to <div> so
  the Layout's own landmarks are not violated.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add coverage for window.__AGENT_CANVAS_AUTH_REQUIRED__ runtime flag

Add isAuthRequired() test block covering the window flag path used by
pre-built static binaries (static-server.mjs --auth-required). Also add
window-flag variants to isAuthRequiredAndMissing() tests.

Addresses review comment #3325587577.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor!: deduplicate SESSION_API_KEY into LOCAL_BACKEND_API_KEY

BREAKING CHANGE: The user-facing env var for setting the API key is now
`LOCAL_BACKEND_API_KEY` everywhere. The old `SESSION_API_KEY`,
`OH_SESSION_API_KEYS_0`, and `VITE_SESSION_API_KEY` env vars are no
longer read by launchers as user-facing configuration.

Internal plumbing (`config.sessionApiKey`, `VITE_SESSION_API_KEY` build
injection, `OH_SESSION_API_KEYS_0` agent-server env) is unchanged — only
the user-facing surface is unified into a single env var.

Changes:
- scripts/dev-safe.mjs: read LOCAL_BACKEND_API_KEY instead of
  SESSION_API_KEY / OH_SESSION_API_KEYS_0 / VITE_SESSION_API_KEY
- scripts/dev-with-automation.mjs: unify key resolution through
  buildSafeDevConfig for both public and local modes
- bin/agent-canvas.mjs: update CLI help text and examples
- docker/entrypoint.sh: read LOCAL_BACKEND_API_KEY, migrate legacy
  session-api-key.txt → api-key.txt
- scripts/static-server.mjs: add mutual-exclusion guard for
  --session-api-key + --auth-required flags
- playwright configs: pass LOCAL_BACKEND_API_KEY instead of the old trio
- test helpers: prefer LOCAL_BACKEND_API_KEY fallback chain
- Update tests and documentation

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review comments — narrow settings probe rethrow and use shared client options

- loadAgentServerInfo: narrow getSettings() catch to rethrow only 401
  errors. Other HTTP errors (403, 5xx) and non-HTTP errors (network,
  timeout) are now swallowed with a console.warn, since the server is
  confirmed up (via /server_info) and the probe is best-effort. This
  prevents misconfigured servers from silently falling through to
  <Outlet /> without showing either the auth or unavailable screen.

- ApiKeyEntryScreen: replace hand-rolled SettingsClient options with
  getAgentServerClientOptions() so transport-level settings (e.g.
  VITE_INSECURE_SKIP_VERIFY) are honoured. Uses the sessionApiKey
  override to pass the freshly-entered key.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: rewrite git+ssh to git+https for @openhands/extensions in lockfile

npm normalizes GitHub URLs to git+ssh:// in the lockfile, but machines
without SSH keys for GitHub (or with stale npm caches) can end up
installing a wrong version of the package. This causes the Vite resolve
error:

  "./integrations" is not exported under the conditions [...]

The same pattern was already fixed for @openhands/typescript-client
(see #384). vercel-install.sh already does a blanket sed rewrite, but
the committed lockfile itself should use git+https:// so local npm ci
works without SSH keys.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: align public auth screen with Add Backend form layout

Replace the custom 'API Key Required' screen with the same form
layout used by the 'Add a Backend' left column in BackendFormModal:

- Heading changed from 'API Key Required' to 'Add a Backend'
- Added backend Name field (required, same as ManualConnectionColumn)
- Host field remains pre-filled and disabled (from window.location.origin)
- API Key field unchanged
- Submit button now uses BACKEND$CONNECT label (matching the modal)
- Removed the subtitle description paragraph for cleaner parity
- Name is persisted to the backend registry on submit

Tests updated: fillApiKey → fillRequiredFields (name + apiKey),
assertions cover the new name field and dual-field submit gating.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: remove unused AUTH$ i18n keys from this PR

The UI refactor (02faa0b0) switched ApiKeyEntryScreen to the
BACKEND$* keys. Drop the three AUTH$ entries that were introduced
and then superseded within this same PR:

- AUTH$API_KEY_REQUIRED_TITLE
- AUTH$API_KEY_REQUIRED_DESCRIPTION
- AUTH$CONNECT

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: sync stale session API key on boot when LOCAL_BACKEND_API_KEY changes

When a user restarts the stack with a different LOCAL_BACKEND_API_KEY,
the new VITE_SESSION_API_KEY is baked in correctly, but localStorage
may still hold the old key in two places:

  1. openhands-agent-server-config.sessionApiKey (written by onboarding
     or the Settings page)
  2. openhands-backends[].apiKey (seeded on first load, never re-synced)

The existing syncDefaultLocalBackendAuth() in storage.ts already tries
to fix #2 by comparing against makeDefaultLocalBackend(), but that
function reads through getConfiguredSessionApiKey() which hits #1
(stale localStorage) before falling back to VITE_SESSION_API_KEY.
So a stale #1 defeats the #2 sync.

Fix: add syncBakedSessionApiKey() which runs from readStoredBackends()
before any key resolution. When VITE_SESSION_API_KEY is set and the
stored key in openhands-agent-server-config differs, overwrite it.
This ensures getConfiguredSessionApiKey() and makeDefaultLocalBackend()
both return the correct key, and the downstream backend-registry sync
works as intended.

Also fix the static-server.mjs injection script to always overwrite a
stored key that differs from the runtime key (was guarded by
`if(!_c.sessionApiKey)` which skipped updates when any key existed).

Add mock-LLM E2E tests for:
- Key rotation recovery: seeds stale localStorage, verifies app loads
- Public-mode auth gate: tests auth screen visibility, wrong key
  rejection, and correct key acceptance

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: document key rotation resilience in AGENTS.md

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add syncBakedSessionApiKey to vi.mock stubs and use click-then-fill in E2E

Three test files mock #/api/agent-server-config without exporting
syncBakedSessionApiKey, which storage.ts now calls at import time.
Add the missing vi.fn() stub to all three.

Also fix the public-mode auth E2E test: use the click() → fill()
pattern for React controlled inputs (matching the established
convention in mock-llm-conversation.spec.ts) so the SettingsInput
onChange fires reliably in Playwright.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add public-mode key rotation E2E test

Simulates a server key rotation: localStorage holds a stale key from
a previous session, the server now has a new key. Verifies the app
detects the 401 from the stale key probe, shows the auth screen, and
accepts the new key.

Flow: stale key in localStorage → probe /server_info → 401 →
isAgentServerAuthError → ApiKeyEntryScreen → user pastes new key →
reload → app loads normally.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(e2e): suppress consent modal in public-mode auth tests

The analytics consent modal overlays the auth screen on first visit
(clean localStorage). Playwright's click() on the form inputs was
intercepted by the modal overlay, causing a 60s timeout loop (121
retries). Add a beforeEach that seeds 'analytics-consent' and
'openhands-telemetry-consent' in localStorage before navigation.

Also deduplicate the consent seeding from the key-rotation test's
addInitScript since the beforeEach now handles it.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: deduplicate readStoredConfig() call in syncBakedSessionApiKey

Capture the first readStoredConfig() result and reuse it in the
spread instead of hitting localStorage twice.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore(docker): remove legacy session-api-key.txt migration

The backwards-compatibility shim that migrated the old
session-api-key.txt to api-key.txt is no longer needed — a breaking
change here is acceptable.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: dedup ApiKeyEntryScreen against BackendForm

ApiKeyEntryScreen now renders BackendForm with three new props instead
of reimplementing the name/host/API-key inputs from scratch:

- hostReadOnly: locks the host field (pre-filled from window.origin)
- requireApiKey: forces a non-empty API key for local backends
- onSubmitOverride: replaces the default sync persist with async
  server-side validation (GET /api/settings) before persisting

The auth-gate-specific chrome (full-screen wrapper, connection status
indicator, validating/error state) stays in ApiKeyEntryScreen via the
existing renderActions slot.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@openhands.dev>
2026-06-01 12:02:37 -06:00
Engel Nystandopenhands ee94749c70 fix: switch conversations by LLM profile name (#978)
Use the agent-server switch_profile endpoint for running conversations so the UI does not need to fetch encrypted profile secrets before switching models.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 16:53:04 +00:00
Tim O'Farrellandopenhands cd03d9cf9c chore: bump version to 1.0.0-alpha.10 (#990)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 23:09:58 +07:00
Tim O'Farrellandopenhands 9285c7a827 fix: set AUTOMATION_AGENT_SERVER_URL in Docker entrypoint to enable local auth (#977)
Without AUTOMATION_AGENT_SERVER_URL, ServiceSettings.is_local_mode returns
False and the automation server tries to validate the session API key against
the OpenHands cloud API (app.all-hands.dev/api/v1/users/me), which returns
401 for locally-generated keys.

Setting AUTOMATION_AGENT_SERVER_URL activates the local-mode fast path in
authenticate_request(), which validates the key directly against
AUTOMATION_LOCAL_API_KEY (already set to the session key) without any
network call. This matches what dev-with-automation.mjs and dev-static.mjs
already do correctly.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 15:42:20 +00:00
f87167deb6 fix: always send frontend's chosen default model to agent-server (#898)
* fix: always send frontend's chosen default model to agent-server (#807)

When the agent-server returns an empty model string (e.g. because no
settings have been saved yet), the adapter's type guard accepted it as
a valid string and forwarded it verbatim. The agent-server would then
fall back to its SDK default ('gpt-5.5') rather than using the
frontend's chosen default ('openhands/minimax-m2.7').

Fix: strengthen the guard in buildConfiguredOpenHandsAgentSettings to
also reject empty/whitespace-only strings, so the frontend's own
default is always sent explicitly:

  llm.model =
    typeof llm.model === 'string' && llm.model.trim().length > 0
      ? llm.model
      : DEFAULT_SETTINGS.llm_model;

While here: align the legacy V0 /api/options/models mock endpoint's
default_model with DEFAULT_SETTINGS (was incorrectly set to claude-opus;
should be minimax-m2.7 to match the rest of the defaults), and document
the canonical default model and the two-location update rule in AGENTS.md.

Closes #807

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor(tests): parameterize llm.model fallback tests + add spec + AGENTS.md

- Extract getModelFrom() helper to eliminate repeated as unknown as ModelPayload casts
- Consolidate 7 individual fallback it() blocks into two it.each() groups:
  (1) agent_settings variants (undefined/empty/whitespace/no-llm-block/empty-settings)
  (2) encryptedAgentSettings variants (empty model / empty object)
- Rename @spec annotation from BM-807 to LLD-001 (not a backend-management spec)
- Add specs/llm-defaults.md with LLD-001 checklist
- Add AGENTS.md note documenting canonical default model, its location, and
  the two-location update rule

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-01 15:29:30 +00:00
0b98b618c1 fix: don't poll git info when no workspace is attached to conversation (#919)
useLocalGitInfo was firing git commands every 10s regardless of whether
the active conversation had an attached workspace, because workingDir
always fell back to getAgentServerWorkingDir() and the queryEnabled
guard had no !!workingDir check.

Mirror the pattern already used in useHasGitCommits: read workingDir
solely from conversation.workspace.working_dir and gate queryEnabled
on !!workingDir. The getAgentServerWorkingDir import is no longer needed
and is removed.

Fixes #776

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@openhands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-06-01 09:20:48 -06:00
DevinandTim O'Farrell efe35446d0 Fix browser panel screenshot flow (#963)
Stop clearing browser state when the Browser tab first mounts, and instead
reset it only when switching to a different conversation. This preserves the
URL and screenshot that were already emitted for the active conversation.

Also update the canvas_ui browser guidance so agents capture a screenshot with
browser_get_state(include_screenshot=true) before opening the Browser tab.
browser_navigate alone only updates the URL, which left the panel blank even
though navigation succeeded.

Add regression coverage for both behaviors:
- preloaded browser screenshots survive first Browser tab mount
- browser state still resets on conversation change
- canvas_ui browser instructions require screenshot capture before opening
  the Browser tab

Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2026-06-01 09:02:52 -06:00
Tim O'Farrellandopenhands 38253e41ba fix: proxy to window.location.origin when local hostnames differ (127.0.0.1 vs localhost) (#975)
shouldUseProxyOrigin() only redirected to window.location.origin when the
browser was at a non-local hostname. When the configured backend uses
127.0.0.1 and the browser accesses via localhost (Docker default), both
are in localHosts so the proxy substitution was skipped — sending the
automation health check directly to http://127.0.0.1:8000 and triggering
a CORS error from http://localhost:8000.

Expand the condition to also proxy when the configured local hostname
differs from the browser hostname (e.g. 127.0.0.1 vs localhost).

Fixes #974

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 08:51:22 -06:00
Hiep Le 032b761984 fix: persist LLM profile changes from all tabs and fix OpenHands round-trip (#970)
* fix: persist LLM profile changes from all tabs and fix OpenHands round-trip

* refactor: update the code based on feedback
2026-06-01 17:36:03 +07:00
117d391bfa test(onboarding): extract shared saved-secret fixture in ACP secrets step tests (#969)
Centralize the "ANTHROPIC_API_KEY already saved + wait for placeholder" setup
into a renderWithSavedApiKey() helper shared by the three existing-secret
tests, and have renderStep own userEvent.setup() so each test no longer
repeats it. Pure test refactor — no production code or behavior change.

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:15:38 +02:00
a46156b231 feat(onboarding): collect ACP provider API keys and store as secrets (#967)
* feat(onboarding): collect ACP provider API keys and store as secrets

Add a credentials step to ACP onboarding for providers that authenticate
via an env-var API key (Claude Code, Codex). The step collects
ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL and OPENAI_API_KEY/OPENAI_BASE_URL
and upserts each filled field as a global secret of the same name, so
they appear under Settings -> Secrets and reach the agent subprocess as
env vars.

- New per-provider secret-field registry in acp-providers.ts
  (getAcpProviderSecrets); single source of truth alongside the existing
  icon and description metadata.
- New SetupAcpSecretsStep rendered on slide 2 of the onboarding modal;
  OpenHands keeps the LLM step, Gemini CLI (OAuth login) skips slide 2.
- Step is skippable: empty fields aren't written; an existing secret
  shows an "already saved" placeholder and is left untouched.
- i18n keys across all 15 locales; tests for the step and modal routing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): mark ACP API-key fields optional

The whole credentials step is skippable (and more so once subscription
auto-detection lands), so the API keys shouldn't read as required either.
Decouple input masking from optionality: a new `secret` flag controls the
password input (API keys) vs plain text (base URLs), and every field now
renders the "Optional" tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(onboarding): assert existing ACP secret is not overwritten when blank

Covers the toSave skip path flagged in review: when a credential already
exists and the user leaves the field blank, handleNext must take the
early-return (no createSecret call) and still advance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: address PR review feedback (#967)

- Narrow SetupAcpSecretsStep's providerKey from string to OnboardingAgentId
  so a mistyped provider key is a compile error rather than a silently empty
  form; matches the type the onboarding modal already tracks.
- Add a key-rotation test: existing secret + typed replacement -> createSecret
  is called with the new value (guards the toSave path against an accidental
  alreadySet skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: Remove PR-only artifacts

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-06-01 09:43:13 +02:00