Commit Graph
56 Commits
Author SHA1 Message Date
9e8b068850 fix(dev): keep tmux sockets under ~/.openhands so macOS won't reap them (#1107)
dev-safe.mjs put the tmux socket dir at os.tmpdir(). On macOS that is the
per-user $TMPDIR (/var/folders/.../T), which the OS periodically reaps
(com.apple.bsd.dirhelper deletes entries untouched for a few days). The reaper
deletes the live tmux socket while the tmux server keeps running, orphaning it,
so every later new-window fails with:

  LibTmuxException: new-window: error connecting to
  .../openhands-agent-canvas-tmux/tmux-501/openhands (No such file or directory)

#325 moved the socket dir to os.tmpdir() to dodge a Unix-domain-socket failure
on mounted volumes, but its intent (per the commit message) was literally /tmp
-- os.tmpdir() != /tmp on macOS, and the per-user temp dir is exactly what gets
reaped.

Default to <stateDir>/tmux (~/.openhands/agent-canvas/tmux) instead, matching
where the rest of dev state already lives: persistent, on local disk, and never
reaped mid-session. dev-safe.mjs only ever runs on the host (the container
launches openhands-agent-server directly, never this script), so the socket dir
is always under a normal local home there.

For the rare host whose $HOME is on a socket-incompatible mount (devcontainers,
NFS/CIFS homes) -- the case #325 cared about -- honor the standard TMUX_TMPDIR
env var (which we already pass through to the agent-server's tmux) so it can be
pointed at a local path like /tmp. No new project-specific env var needed.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:07:13 +00:00
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
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
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
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
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
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
Tim O'Farrellandopenhands eb3ae22b23 fix: fast-fail dev scripts when ports are already in use (#939)
* fix: fast-fail dev scripts when ports are already in use

Add `assertPortsFree` to `scripts/dev-safe.mjs` that checks each
preferred port and throws a descriptive error if any are already bound,
rather than silently binding to an alternative port.

- `buildSafeDevConfigAsync` now calls `assertPortsFree` before
  returning config, so `npm run dev:minimal` exits immediately with a
  clear message when the agent-server port is occupied.
- `dev-with-automation.mjs`'s `buildConfig` does the same for all four
  service ports (ingress, agent-server, automation, vite), covering
  `npm run dev` and `npm run dev:static`.
- `buildConfig` gains env-var overrides for internal service ports
  (`OH_CANVAS_SAFE_BACKEND_PORT`, `OH_CANVAS_SAFE_AUTOMATION_PORT`,
  `OH_CANVAS_SAFE_VITE_PORT`) so tests and advanced users can redirect
  ports without touching production defaults.

Tests updated accordingly:
- Old "falls back when port is busy" tests replaced with "throws when
  port is busy" equivalents.
- New `assertPortsFree` describe block with three targeted cases.
- `envWithIsolatedKeyPath` in `dev-with-automation.test.ts` now seeds
  high free ports so the pre-flight check passes when a real dev stack
  is running during local test execution.

Closes #934

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

* refactor: address review bot suggestions on assertPortsFree

- Check ports in parallel with Promise.all instead of sequentially
  (each check is independent I/O, so parallel is faster and more idiomatic)
- Include vscode port in the buildSafeDevConfigAsync pre-flight check
  alongside the agent-server port, so a conflict on that internal port
  is also caught with a helpful message rather than a cryptic spawn error

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-29 17:25:52 +00:00
665a258b80 feat(acp): inline live model picker for ACP conversations (#769) (#832)
* feat(acp): inline live model picker for ACP conversations (#769)

Converge ACP model selection onto the native LLM-profile inline picker UX
with live mid-conversation switching, replacing the display-only popover.

- Bump @openhands/typescript-client 1.23.3 -> 1.24.0 (adds switchAcpModel).
- AgentServerConversationService.switchAcpModel(conversationId, model): POST
  /switch_acp_model via ConversationClient, with switchProfile's local-only guard.
- useSwitchAcpModel hook: live switch for a running ACP session; for the
  home/no-session case, persist the choice as the agent-settings default
  (agent_settings_diff { acp_model }) so the next conversation inherits it.
- ChatInputModel popover becomes a picker over the provider's available_models
  (check on the effective model), local backend only; cloud / custom-provider /
  native surfaces keep the display + Settings link.
- New i18n key MODEL$AVAILABLE_MODELS.
- Tests for the hook (live vs settings-default branches) and the picker.

Local backend only (matches native switching); custom/unknown providers and any
app_server route remain out of scope per #769.

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

* fix(acp): open the model picker on click (don't self-close via click-outside)

The inline picker's trigger button sits outside the popover element, so the
document click-outside handler (useClickOutsideElement) treated the opening
click as an "outside" click and closed the popover in the same interaction —
clicking the chip appeared to do nothing. (A programmatic el.click() worked by
fluke: the popover isn't rendered yet when that click bubbles, so the ref is
null and the close is skipped.)

Pass the trigger button as the hook's ignoreOutsideClickRef so a click on the
chip toggles the popover instead of being treated as an outside click.

Validated end-to-end against a local agent-server 1.24.0: the picker opens and
lists the provider's available_models, and selecting one writes the default via
PATCH /settings (home case), with the chip updating to the new model.

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

* refactor(acp): drop disableToast in useSwitchAcpModel so switch errors surface

useSwitchLlmProfile sets meta.disableToast because it's wrapped by
useSwitchLlmProfileAndLog, which re-surfaces errors via its own onError.
useSwitchAcpModel is called directly (no such wrapper / no onError), so
disableToast was silently swallowing failed switches and settings writes
(e.g. a 409 before the first message, network errors, the cloud guard).

Remove it and let the global mutation error toast report failures — simpler
and gives the user feedback when a switch doesn't take.

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

* refactor(acp): share chat input model picker state

* chore: address PR review feedback (#832)

- Add unit test for useChatInputModelState pinning its branching contract,
  incl. the active-ACP getAcpProvider lookup (was home-only in old component).
- Document why the overflow model submenu uses overflow-y-auto (scroll long
  model lists) rather than overflow-visible — no floating children to clip.

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

* chore: address PR review feedback (#832)

- Wrap the 'Available models' section label in a presentational <li> so it
  is a valid child of the ContextMenu <ul> (was a bare <div>).
- Drop unnecessary 'as never' casts in use-switch-acp-model tests now that
  the real return types (Promise<void>, Promise<boolean>) are honored.

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

* fix(acp): bump agent-server pin to 1.24.0 for /switch_acp_model

The inline ACP model picker POSTs to /api/conversations/{id}/switch_acp_model,
which is new in openhands-agent-server 1.24.0. The PR description already
lists agent-server:1.24.0 as a dependency, but config/defaults.json was
left at 1.23.1, so local dev (npm run dev) and Docker installs would 404
on every model switch attempt.

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

* fix(settings): always land on /settings/agent from /settings

The fallback order in ``getFirstAvailablePath`` put ``/settings/llm``
first whenever ``hide_llm_settings`` was off, so clicking Settings sent
the user to the LLM page. For ACP users that page is disabled and
``redirectIfAcpActive`` only catches them when the *personal* settings
already say ``agent_kind === "acp"`` — being in an ACP conversation
with non-ACP personal settings (the common case during the inline
picker flow) bypassed the guard and dumped them on /settings/llm.

Make ``/settings/agent`` the unconditional first fallback. It is
always available (no feature flag hides it), houses the agent-kind
picker, and the left nav still gets OpenHands users to LLM in one
click — so one extra click for non-ACP users buys a much simpler
routing surface and kills the ACP misroute.

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

* fix(settings/agent): clear command when switching to Custom preset

Selecting "Custom" in the agent preset dropdown reset ``acpModel`` and
flipped ``isCustomAcpModel`` but left ``commandText`` untouched. On the
next render, ``detectPreset(commandText, ACP_PROVIDERS)`` still matched
the previous provider's ``default_command`` and snapped the dropdown
back off "Custom" — the toggle never stayed on Custom.

Clear ``commandText`` in the Custom branch so ``detectPreset`` falls
through to ``ACP_CUSTOM_PRESET_KEY`` on the next render and the dropdown
stays where the user put it. Empty command also matches the intended
"user supplies their own" semantics of the preset.

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

* fix(settings): mark Verification page as disabledByAcp

The Verification page writes ``confirmation_mode`` and
``security_analyzer`` into ``conversation_settings_diff``. The ACP
agent loop never reads either: ``openhands/sdk/agent/acp_agent.py``
has zero references to ``confirmation_policy`` or
``security_analyzer``, and the only runtime readers
(``openhands/sdk/agent/agent.py:844,855``) live on the native
``Agent`` class — not on ``ACPAgent``. The backend accepts the values
and stores them on conversation state, but the ACP subprocess never
consults them.

So the page presents real-looking knobs that silently do nothing for
ACP users. Mark it ``disabledByAcp: true`` — same pattern as
``/settings/llm`` and ``/settings/condenser`` — so it greys out in the
nav and the existing route guard at ``src/routes/settings.tsx:47-51``
bounces direct visits to ``/settings/agent``.

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

* fix(ci): bump doc/script SDK version examples to 1.24.0

The docs-version-sync test enforces that every documented agent-server
version example matches ``config/defaults.json:versions.agentServer``.
The previous commit bumped that pin from 1.23.1 to 1.24.0 for the
``/switch_acp_model`` route, but left the example references in
AGENTS.md, ``scripts/dev-safe.mjs``, and ``scripts/check-sdk-version-sync.mjs``
behind — the drift-detector caught it as ``test-and-build`` failure.

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

* fix(ci): bump remaining hard-coded 1.23.1 to 1.24.0

``__tests__/scripts/dev-safe.test.ts`` asserts ``buildAgentServerCommand``'s
default ``uvx`` args literally include ``openhands-agent-server==1.23.1`` and
matching ``openhands-{sdk,tools,workspace}==1.23.1``. The CI fix in the prior
commit only updated docs and example references; the central pin bump in
``config/defaults.json`` flowed through to this test's runtime expectation but
the literal expectations were never updated. Bump them.

Also bump the ``MOCK_AGENT_SERVER_VERSION`` placeholder in
``src/mocks/settings-handlers.ts`` for consistency with the central pin —
no test asserts on it, but leaving the mock at 1.23.1 invites future
drift confusion.

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

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:42:17 +02:00
b570b4a2f6 fix: buildNpmScriptCommand always uses cmd.exe on Windows (#734)
* fix: buildNpmScriptCommand always uses cmd.exe on Windows

On Windows, npm sets npm_execpath to a path like
  C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js
which contains spaces. buildNpmScriptCommand was returning that
path as a spawn argument with the full node.exe path as the command.
spawnService uses shell:true on Windows, so Node.js passes the
unquoted command to cmd.exe:

  cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ...

cmd.exe splits on the space and fails with
  'C:\Program' is not recognized as an internal or external command
causing Vite to exit with code 1 immediately after npm run dev.

Fix: check platform === 'win32' BEFORE checking npm_execpath so
Windows always uses the safe cmd.exe /d /s /c npm run <script> form.

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

* fix: buildNpmScriptCommand always uses cmd.exe on Windows

On Windows, npm sets npm_execpath to a path like
  C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js
which contains spaces. buildNpmScriptCommand was returning that
path as a spawn argument with the full node.exe path as the command.
spawnService uses shell:true on Windows, so Node.js passes the
unquoted command to cmd.exe:

  cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ...

cmd.exe splits on the space and fails with
  'C:\Program' is not recognized as an internal or external command
causing Vite to exit with code 1 immediately after npm run dev.

Fix: check platform === 'win32' BEFORE checking npm_execpath so
Windows always uses the safe cmd.exe /d /s /c npm run <script> form.

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

* fix: AnimatePresence mode=wait expects one child, not two

ChatStatusIndicator had two separately-keyed motion.span children inside
<AnimatePresence mode="wait">. framer-motion's wait mode expects exactly ONE
child to exit before the next enters; two children trigger the repeated warning:
  "attempting to animate multiple children within AnimatePresence,
   but its mode is set to 'wait'"

Fix: wrap both elements in a single motion.span with unified key={status}
and className="contents" (CSS display:contents preserves flex layout).

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

* fix: set PYTHONUTF8=1 in agent-server/automation env on Windows

Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server
writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode,
producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation
files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables
Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour.

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

* fix: set PYTHONUTF8=1 in agent-server/automation env on Windows

Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server
writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode,
producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation
files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables
Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour.

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

* refactor: remove unrelated file

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-28 16:29:55 +07:00
cbeeee002e fix: inject runtime session key into index.html for published binary (#795)
* fix: inject runtime session key into index.html for published binary

The globally installed agent-canvas binary starts the agent-server with a
persisted session API key (~/.openhands/agent-canvas/session-api-key.txt)
as OH_SESSION_API_KEYS_0, making auth required. However, the pre-built
static frontend in the npm package has a different (or empty)
VITE_SESSION_API_KEY baked in at publish time, so every API request gets
401 Unauthorized.

Fix: static-server.mjs now accepts --session-api-key <key> and injects a
tiny bootstrap <script> before </head> in every index.html response. The
script seeds the key into localStorage['openhands-agent-server-config']
only if no key is already stored there, so explicit user overrides (via
Settings > Agent Server) are always preserved.

dev-with-automation.mjs and dev-static.mjs both pass
--session-api-key ${config.sessionApiKey} when spawning the static server,
so the runtime key is always available regardless of what was baked into
the bundle.

Tests: added 8 new cases to __tests__/scripts/static-server.test.ts
covering parseArgs, injection in direct and SPA-fallback index.html
responses, no injection for non-html assets, cache headers, and null key.

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

* fix(docker): pass runtime session key to static-server so frontend can authenticate

The entrypoint computed EFFECTIVE_SESSION_KEY and forwarded it to the
agent-server (OH_SESSION_API_KEYS_0) and automation backends, but did not
pass it to the static-server. As a result the pre-built index.html served
with no session key injected, so every browser API call received 401.

Wire --session-api-key "$EFFECTIVE_SESSION_KEY" into the static-server
launch command so the runtime key is injected into index.html responses
(via the mechanism added in this branch to static-server.mjs).

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

* fix: address review suggestions on session key injection

- static-server.mjs: add comment clarifying replace() targets first
  </head> only; fall back to inserting before </body> when </head> is
  absent (avoids prepending before <!DOCTYPE html>)
- docker/entrypoint.sh: add comment documenting source of
  EFFECTIVE_SESSION_KEY before the static-server invocation
- static-server.test.ts: add test for </head>-absent fallback path
  confirming injection lands before </body>

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-05-27 11:12:23 -04:00
Rohit Malhotraandopenhands e2dd1b5f17 fix: unify session and automation API keys into a single credential with consistent header (#681)
* fix: unify session and automation API keys into a single credential

Both the agent-server and automation backend now share the same API key
value. The agent-server validates it via `X-Session-API-Key` and the
automation backend validates it via `Authorization: Bearer …` — different
header formats, same credential.

Changes:
- Frontend: automation axios client reads `VITE_SESSION_API_KEY` instead
  of the now-removed `VITE_AUTOMATION_API_KEY`
- Dev launcher: removed separate `AUTOMATION_LOCAL_API_KEY` generation
  and persistence (`automation-api-key.txt`); `localApiKey` is set to
  `sessionApiKey` so both backends receive the same value
- Static build: stopped baking `VITE_AUTOMATION_API_KEY` (the frontend
  reads from `VITE_SESSION_API_KEY`)
- Docker entrypoint: `OPENHANDS_AUTOMATION_API_KEY`,
  `AUTOMATION_LOCAL_API_KEY`, and `AUTOMATION_AGENT_SERVER_API_KEY` all
  default to the session key when not explicitly overridden
- Tests updated to verify unified key behavior

Fixes the 401 on `/api/automation/v1` when the automation backend is
running but no separate `VITE_AUTOMATION_API_KEY` was configured.

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

* fix: use X-Session-API-Key header for automation backend auth (consistent with agent-server)

Switch automation backend requests from `Authorization: Bearer …` to
`X-Session-API-Key` header, matching the agent-server's auth pattern.
Both backends now authenticate using the same header and the same key
value (`VITE_SESSION_API_KEY`).

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

* fix: address review — remove localApiKey alias, dead constant, add entrypoint guard

- Remove `localApiKey` from config; all call sites now use
  `config.sessionApiKey` directly, making the unified-key intent obvious.
- Delete `DEFAULT_AUTOMATION_API_KEY_PATH` constant and its export
  (no downstream consumers in beta).
- Add fail-fast guard in docker/entrypoint.sh when no session key is
  available, instead of silently exporting empty strings.

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

* fix: update stale comment on AUTOMATION_LOCAL_API_KEY to reflect unified session key

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 22:15:27 +00:00
Rohit Malhotraandopenhands 45da5606d6 fix: bump agent-server SDK to 1.23.1 (#782)
Fixes two bugs in the upstream agent-server SDK v1.23.0 that caused
500 Internal Server Error on POST /api/conversations:

1. LLM registry duplicate usage_id: The condenser and main agent LLMs
   both used usage_id='default', causing ValueError on conversation
   creation. v1.23.1 checks for existing usage IDs before registering.

2. Validation error handler crash: The _validation_exception_handler
   tried to JSON-serialize raw ValueError objects from Pydantic
   validation contexts, turning 422 errors into 500s. v1.23.1 properly
   sanitizes validation errors before serializing.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 18:42:17 +00:00
cc63f48a07 refactor(acp): source ACP model lists from typescript-client (closes #740) (#775)
* refactor(acp): source model lists from typescript-client registry

Replace the hand-mined CLAUDE_MODELS / CODEX_MODELS / GEMINI_MODELS lists (and
the duplicated provider metadata) with the @openhands/typescript-client ACP
registry, which mirrors the Python SDK source of truth
(openhands.sdk.settings.acp_providers). acp-providers.ts becomes a thin
adapter: it enriches each upstream record with Canvas-only UI fields (brand
icon + onboarding description) and keeps the helper functions + public export
surface unchanged, so no consumers change.

- Bump the @openhands/typescript-client pin to the #187 merge commit
  (082d4d46), which adds available_models / default_model to the registry.
- Delete the three hardcoded model lists; build ACP_PROVIDERS from
  getAcpProvider() + a small ACP_PROVIDER_UI map.
- Incidentally corrects the Gemini default to auto-gemini-2.5 (the CLI's
  auto-router default), matching the merged SDK/client.

Closes #740.

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

* chore(acp): pin typescript-client to v1.23.2 tag (was SHA)

Now that typescript-client v1.23.2 is tagged/released (includes #187's ACP
registry, mirroring SDK #3389), pin to the tag instead of the raw #187 merge
SHA. v1.23.2 tracks the SDK's v1.23.2 patch line. Resolves to the same commit
as the prior SHA, so no resolved-content change.

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

* ci(acp): remove obsolete ACP providers sync check

The acp-providers-sync workflow + scripts/check-acp-providers-sync.mjs existed
to keep Canvas's hand-kept ACP registry mirror in sync with the SDK source
(agent-canvas#587). That mirror is gone — acp-providers.ts now sources its
model data from @openhands/typescript-client, which carries its own
SDK-drift check (check-acp-drift.py). So this canvas-side check is redundant
and was failing on the refactored ACP_PROVIDERS (no longer a literal array).

- Delete .github/workflows/acp-providers-sync.yml + the script.
- Drop the docs-version-sync test case that asserted the script's example.

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

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:25:08 +02:00
Rohit Malhotraandopenhands 89abe93a28 chore: bump automation version to 1.0.0a5 (#774)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 16:27:21 +00:00
Tim O'Farrellandopenhands c9616c1153 fix: pin openhands-sdk to same version as other SDK packages (#772)
Add openhands-sdk==${VERSION} to the --with list in both PyPI
resolution branches (specific version and default) of
buildAgentServerCommand so all four packages are pinned to the
same versions.agentServer:

  openhands-agent-server, openhands-sdk, openhands-tools, openhands-workspace

Without this pin, openhands-sdk floated to the latest release on
PyPI (a transitive dep with no version bound in the published
openhands-agent-server metadata), causing non-reproducible builds
and version skew between the banner and defaults.json.

The local-path (editable) and git-ref branches are unaffected —
they already source all packages from the same checkout/ref.

Update the two affected unit-test cases to expect the new pinned
openhands-sdk entry in the args array.

Fixes #767

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 15:38:53 +00:00
Hiep Le e54b04cf38 chore: refresh stale 1.22.1 doc examples after agent-server bump to 1.23.0 (#694) 2026-05-21 12:49:04 +07:00
Rohit Malhotra eee7fe1b7a feat: use async conversations and interrupt endpoint for local mode (#670) 2026-05-20 22:16:20 -04:00
Rohit Malhotraandopenhands ad5c5533a9 fix(tests): fix two flaky CI tests on Windows (#667)
conversation-panel: createMockConversation() gave all conversations the
same updated_at timestamp (new Date()), making sort order non-deterministic.
Tests that indexed into cards[0] expecting conversation "1" would sometimes
get conversation "2" instead.  Fix: use a monotonic counter so each call
produces a timestamp 1 s older than the previous, guaranteeing array
insertion order matches sort order.

dev-with-automation: the OH_AGENT_SERVER_LOCAL_PATH test created a Unix
shell stub for uvx and joined PATH with ':'.  On Windows the PATH separator
is ';', the stub needs a .cmd extension, and where.exe needs PATHEXT and
SystemRoot to function.  Fix: use path.delimiter, create uvx.cmd on Windows,
and pass through the required Windows env vars.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-20 11:10:48 -04:00
Hiep Le f42d684a03 fix(frontend): validate OH_AGENT_SERVER_LOCAL_PATH in dev-with-automation (#650)
* fix: validate OH_AGENT_SERVER_LOCAL_PATH in dev-with-automation

* fix: failing tests
2026-05-20 15:29:24 +07:00
Rohit Malhotraandopenhands edb998220a Remove Docker dependency from dev workflow (#635)
- Delete scripts/dev-docker.mjs and its test
- Simplify package.json: 'npm run dev' now runs local uvx stack directly
  (agent-server + automation + Vite + ingress), no Docker needed
- Remove dev:docker, dev:docker:dynamic, dev:dangerously-dockerless scripts
- Add dev:static for production-build frontend variant
- Update bin/agent-canvas.mjs CLI to use uvx-based stack
- Rename Docker-specific variables: DOCKER_PROJECTS_PATH → PROJECTS_PATH,
  shouldDefaultToDockerProjects → shouldDefaultToProjectsPath
- Update i18n: HOST_HOME_NOT_MOUNTED_HINT no longer references Docker
- Update all docs (README, DEVELOPMENT, SELF_HOSTING, AGENTS.md, CHANGELOG)
- Rename e2e snapshot: docker-workspace-browser → projects-workspace-browser
- Fix all tests to reflect new script names and remove Docker references

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-19 15:27:25 -04:00
90ad71dbd9 Add recommended automations and MCP marketplace setup flow (#504)
* Add recommended automations marketplace flow

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

* Update GitHub MCP QA findings

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

* Polish MCP and automation marketplace UI

Add a shared MCP logo badge, make marketplace cards more compact, and show required MCP logos prominently on recommended automation cards. Update the extensions package lock to the per-entry catalog split and save QA screenshots/results in .pr/.

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

* Align recommended automations styling

Remove the custom gradient treatment and match the recommended automation cards and setup modal to the existing Automations and MCP page surfaces, spacing, borders, and typography.

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

* Show existing automations before recommendations

Move the recommended automations section below the current automation list and creation guidance so the page prioritizes the user existing automation state.

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

* Add recommendations to onboarding

Show recommended automations below the Say Hello input so new users can launch a curated automation from the final onboarding step.

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

* Use extensions MCP marketplace exports

Remove the local MCP marketplace wrapper and consume MCP catalog data plus logo mappings directly from @openhands/extensions/mcps.

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

* Archive previous PR QA and add refreshed artifacts

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

* Add scheduled automation QA evidence

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

* Streamline recommended automation launch

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

* Refresh QA evidence and remove old artifacts

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

* Ensure canvas tools are on Python path

* Require MCP installs before launching recommendations

* Remove archived PR artifacts

* Drop redundant PYTHONPATH launcher changes

* Inline recommended automation catalog

* Point extensions dependency at main

* Augment recommended automation prompts with explicit API instructions

When a recommended automation is selected, the pre-filled prompt now
includes backend-specific API instructions so the agent calls the
correct endpoint:

- Local backends: directs the agent to use the local automation API
  from <RUNTIME_SERVICES> with $OPENHANDS_AUTOMATION_API_KEY auth,
  and explicitly tells it NOT to call the cloud API at app.all-hands.dev.

- Cloud backends: directs the agent to use the OpenHands Cloud
  Automations API at app.all-hands.dev with Bearer $OPENHANDS_API_KEY.

The buildAutomationPrompt() helper is exported for testability.
Three new unit tests cover both backend kinds and prompt preservation.

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

* Fix recommended automation launch regressions

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

* Expose local automation API key to agent terminals

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

* Stabilize conversation panel stop menu test

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

* chore: Remove PR-only artifacts

* fix: pin @openhands/extensions to specific commit for reproducibility

Updates the dependency from #main to the exact commit SHA
(3bba8e3b) that contains the MCP and automation catalogs
added in extensions#237.

This ensures reproducible builds since npm ci will use the
locked SHA instead of potentially picking up a newer main.

* Address final automation marketplace review comments

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
2026-05-19 15:04:53 -04:00
Tim O'Farrellandopenhands 0cf58f8657 fix(dev-docker): allow exec on the home tmpfs so stdio MCP servers work (#535)
Docker's `--tmpfs` flag defaults its mount option set to
`rw,noexec,nosuid,nodev`. When dev:docker overlays the agent-server
container's `/home/openhands` with a tmpfs (so the mapped host user can
own it), it was inheriting that `noexec` flag unintentionally.

That breaks any stdio MCP server installed via npx -- e.g.
`npx -y @modelcontextprotocol/server-github` caches its binary under
`~/.npm/_npx/<hash>/node_modules/.bin/mcp-server-github`, npx then tries
to exec it, and the kernel returns EACCES regardless of the 0755 mode
bits on the file. The user sees:

    sh: 1: mcp-server-github: Permission denied
    Failed to connect to MCP server 'github', skipping
    ...
    MCPError: MCP Connection Failure

and the conversation that triggered it aborts during agent init.

Pass `exec` explicitly to override only the `noexec` default. We keep
`nosuid` and `nodev` (the home dir has no business hosting setuid
binaries or device nodes) so we lose no defense-in-depth beyond what's
necessary to make the supported MCP integration actually function.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-16 21:15:42 -06:00
Engel Nystandopenhands 5c42bd3492 Rename PROJECT_PATH to PROJECTS_PATH (#521)
* Rename PROJECT_PATH to PROJECTS_PATH

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

* Apply suggestion from @enyst

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-16 16:20:44 +00:00
Hiep Le 5c33c10b18 feat(frontend): add canvas_ui tool so the agent can drive the UI (#420)
* feat: add canvas_ui tool so the agent can drive the UI

* refactor: update the code based on feedback

* fix: failing tests

* refactor: update the code based on feedback
2026-05-16 16:09:43 +07:00
Tim O'Farrellandopenhands b2b71855c6 feat(dev): surface dev-stack runtime services in agent system prompt (#503)
* feat(dev): surface dev-stack runtime services in agent system prompt

Add a structured 'runtime services' info object that the dev launchers
(`dev:safe`, `dev:automation`, `dev:docker`, and the published
`agent-canvas` binary) propagate to the frontend via
`VITE_RUNTIME_SERVICES_INFO`. The frontend renders it into a
`<RUNTIME_SERVICES>` markdown block and attaches it as
`AgentContext.system_message_suffix` on every `POST /api/conversations`.

This means agents start each conversation knowing exactly what services
exist in the current dev stack (ingress URL, automation backend URL +
`/api/automation` prefix, auth header, etc.), instead of having to probe
or — worse — assume `localhost:8000` is the automation server when it is
actually the Agent Server they are running inside of.

URLs are written from the agent's point of view: dockerless modes use
`localhost`, `dev:docker` uses `host.docker.internal`. When automation
isn't running in the current mode (e.g. `dev:safe`), the block says so
explicitly so agents know to skip `/api/automation` calls.

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

* fix(runtime-services): address review feedback on PR #503

- Validate required `agentServerPort` in `buildRuntimeServicesInfo`;
  previously a missing port baked `http://localhost:undefined` into
  the agent's system prompt.
- Skip the automation entry when the supplied `automation` object has
  no `port` (e.g. a bare `{}` from a misconfigured launcher).
- Rename the JSON service key from `vite` to `frontend` and add a
  `kind: "vite" | "static"` discriminator + mode-aware description,
  so static-build dev stacks (`dev:docker`, the published binary, ...)
  no longer surface a misleading "Vite dev server" line in the agent
  system prompt. The renderer still accepts the legacy `vite` key.
- Anchor the "don't guess" warning to the actual agent-server URL from
  runtime info instead of hardcoded `localhost:8000`, since the
  agent-server uses different ports across dev modes (18000 in
  dev:safe, 8000 in dev:docker, ...).
- Plumb `frontendKind` through `buildAutomationRuntimeServicesInfo`
  and stamp `config.frontendKind` in `dev-with-automation.mjs::main`
  so both Vite spawn and static-build paths describe the frontend
  correctly.
- Expand AGENTS.md with the JSON schema of `VITE_RUNTIME_SERVICES_INFO`
  and a concrete example of the rendered `<RUNTIME_SERVICES>` block.
- Tests: assert the new URL-in-warning behavior, the new `frontend` /
  legacy `vite` rendering, the `agentServerPort`-required guard, the
  `automation: {}` skip, and the legacy `vitePort` alias.

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 20:53:45 -06:00
Hiep Le 2d5a52d4be chore: bump agent-server default to 1.22.1 (#477)
* chore: bump agent-server default to 1.22.1

* chore: update OH_AUTOMATION_VERSION

* chore: update DEFAULT_AUTOMATION_SDK_VERSION
2026-05-16 02:38:59 +07:00
2899c7b383 Fix static automation auth and switch LLM tool (#474)
* Fix static automation auth and switch LLM tool

* Allow automation SDK release lag in sync check

* chore: update baseline snapshots [skip ci]

* chore: trigger CI after snapshot update

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 15:30:29 +00:00
Xingyao Wangandopenhands e5711e74b2 Clean up dev stack process trees on shutdown (#475)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 10:48:28 -04:00
Graham Neubig 5b05321483 Fix Windows static asset serving (#456) 2026-05-15 01:24:11 -04:00
Xingyao Wang 0fc85df818 Run Docker dev server with host user and tmpfs home (#443)
* Run Docker dev server as host user

* Use tmpfs for Docker agent home

* Document Docker tmpfs home rationale
2026-05-14 16:15:03 -04:00
Graham Neubigandopenhands 3f28f2d625 Fix static automation agent-server auth (#442)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 14:54:04 -04:00
Graham Neubigandopenhands a4089e0e4a Default user launchers to static frontend (#434)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 18:18:36 +00:00
Graham Neubigandopenhands 5ccc8e627c Fail fast when frontend deps are missing (#404)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 10:32:03 -04:00
Graham Neubigandopenhands 2b40e3425e Clarify Docker permission errors in dev script (#403)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-13 00:00:42 -04:00
Rohit Malhotraandopenhands 773cc72324 feat: update SDK to 1.22.0 and add CI version sync check (#333)
* feat: update SDK to 1.22.0 and add CI version sync check

- Update DEFAULT_AGENT_SERVER_VERSION from 1.21.1 to 1.22.0 in dev-safe.mjs
- Update SDK version references in AGENTS.md
- Add scripts/check-sdk-version-sync.mjs to verify automation project uses
  matching SDK versions for openhands-sdk, openhands-tools, openhands-workspace,
  and openhands-agent-server
- Add .github/workflows/sdk-version-sync.yml CI workflow with:
  - Path-filtered PR/push triggers for version-related file changes
  - repository_dispatch triggers (sdk-version-check, sdk-release) for
    external repos to notify when SDK deps change
  - workflow_dispatch with optional version override
  - Scheduled runs every 6 hours to catch upstream changes
  - PyPI version checking support (--check-pypi flag)

The check script supports:
- EXPECTED_SDK_VERSION env var override for CI triggers
- --check-pypi flag to also display latest PyPI versions
- --help for usage documentation

To trigger from external repos (e.g., OpenHands/automation or SDK repo):
  curl -X POST -H "Authorization: token \$GITHUB_TOKEN" \\
    https://api.github.com/repos/OpenHands/agent-canvas/dispatches \\
    -d '{"event_type": "sdk-version-check"}'

* fix: check released PyPI version instead of GitHub main branch

The SDK version sync check now fetches dependencies from the released
openhands-automation package on PyPI (version specified by
DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs) rather than
fetching pyproject.toml from the GitHub main branch.

This ensures we're checking the actual released version that users
would install, not the development version on main.

* fix: address review feedback for SDK version sync check

- Add env var overrides for automation package name and version
- Add retry logic with exponential backoff for PyPI API failures
- Add semantic version normalization for comparing versions
- Fix repository_dispatch to use client_payload.version
- Improve regex to handle parenthesized dependency formats
- Add comprehensive test coverage for helper functions

* fix: add type casts for dynamic module import in tests

* chore: update automation version to 1.0.0a2

- Update DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs
- Update AGENTS.md documentation
- Update test expectation

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-11 15:39:17 -04:00
Xingyao Wangandopenhands 18e51fa84d fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes (#325)
* fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes

Some filesystems (NFS, CIFS, certain FUSE/overlay mounts used by Docker
bind-mounts) do not support Unix domain sockets. When TMUX_TMPDIR pointed
to ~/.openhands/agent-canvas/tmux/ inside a container, tmux failed with:

  error connecting to .../tmux-10001/openhands (Operation not supported)

Move tmux socket directory to /tmp/openhands-agent-canvas-tmux which is
always on a local/tmpfs filesystem that supports Unix sockets. Tmux
sockets are ephemeral and don't need persistence across restarts.

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

* refactor: drop explicit TMUX_TMPDIR from dev-docker.mjs, use system default

Per review feedback — the container's default TMUX_TMPDIR (/tmp) already
supports Unix domain sockets, so there's no need to set it explicitly.

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-11 13:37:25 -04:00
Robert Brennanandopenhands 31302f9282 fix(ingress): handle socket errors so ECONNRESET can't crash the proxy (#280)
A WebSocket flowing through scripts/ingress.mjs would take down the
whole ingress process whenever its underlying TCP socket reset:

  Error: read ECONNRESET
      at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
  Emitted 'error' event on Socket instance at:
      at Socket.onerror (node:internal/streams/readable:1026:14)

proxyWebSocket() only attached an 'error' listener to the outbound
HTTP upgrade request, never to the raw client / upstream sockets that
the bidirectional pipe runs over. ECONNRESET on a long-lived
/sockets/events/... connection (browser tab close, NAT timeout,
mobile network handoff, …) therefore became an unhandled 'error' event
on a Socket and Node aborted the process.

Fix:
  - Attach 'error' (and 'close') listeners to both the client socket
    and the upstream socket in proxyWebSocket; on either side erroring,
    tear the peer down gracefully.
  - Mirror the same defensive handling for plain HTTP in proxyRequest:
    add 'error' handlers on req, res, and proxyRes so a mid-stream
    disconnect aborts the upstream call instead of crashing.
  - Add server.on('clientError') for malformed client requests.
  - Add a narrow uncaughtException guard that swallows benign socket
    teardown errors (ECONNRESET / EPIPE / ECONNABORTED /
    ERR_STREAM_PREMATURE_CLOSE) but rethrows everything else, so real
    bugs stay visible.

Tests:
  - New regression covering an upstream WebSocket that immediately RSTs
    after upgrading; before the fix this took the proxy down (next
    request fails with ECONNREFUSED), after the fix the process keeps
    serving HTTP traffic and stderr never shows "Unhandled 'error' event".
  - New regression covering a client that aborts an in-flight HTTP
    request mid-flight.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-10 14:08:21 -07:00
Hiep Le 137fbae87f fix: pass container workspaces path as VITE_WORKING_DIR (#259) 2026-05-10 15:56:33 +07:00
Robert Brennanandopenhands 5d16a6cbcf Persist dev session API key to disk and re-seed default backend (#240)
Both `npm run dev` (Docker) and `npm run dev:dangerously-dockerless`
previously generated a fresh random SESSION_API_KEY per process. The
key was passed to the agent-server (OH_SESSION_API_KEYS_0) and to Vite
(VITE_SESSION_API_KEY), but the frontend's `openhands-backends`
localStorage entry was seeded only on the very first load. After a
single restart, the persisted entry's `apiKey` no longer matched the
agent-server, leading to 401s until the user manually edited the
backend.

Fix this by giving `buildSafeDevConfig` a stable default:

- `getOrCreatePersistedSessionApiKey()` reads / creates
  `~/.openhands/agent-canvas/session-api-key.txt` (mode 0600). The
  in-memory cache is keyed by path so tests can use `mkdtemp` paths.
- `OH_SESSION_API_KEY_PATH` env var overrides the file location
  (used by tests; can also be used to pin in unusual setups).
- Existing env overrides (SESSION_API_KEY / OH_SESSION_API_KEYS_0 /
  VITE_SESSION_API_KEY) still take precedence.

Because dev:docker and dev:dangerously-dockerless both flow through
the shared `buildSafeDevConfig`, they automatically pick up the
same persisted key and stay in sync with the Vite-baked
VITE_SESSION_API_KEY.

On the frontend, `readStoredBackends` now also re-seeds the default
Local backend when storage parses to `[]` or contains only invalid
entries (previously only `null` triggered seeding). This is safe now
that the persisted key keeps the seed valid across restarts.

Tests:
- New `getOrCreatePersistedSessionApiKey` tests covering creation,
  reuse, whitespace trimming, and empty-file regeneration.
- New `buildSafeDevConfig` / `buildConfig` tests covering the
  on-disk fallback, restart parity (dev:docker vs
  dev:dangerously-dockerless), and env-override precedence.
- New backend-registry storage tests covering re-seed on missing,
  empty, and all-invalid storage states.
- Existing tests that previously hit the real
  `~/.openhands/agent-canvas/session-api-key.txt` were updated to
  use isolated `OH_SESSION_API_KEY_PATH` temp dirs.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-09 13:54:14 -07:00
Rohit Malhotraandopenhands 3207d90e72 feat: add dynamic port allocation with preferred port fallback (#223)
* feat: add dynamic port allocation with preferred port fallback

Implement dynamic port allocation for dev entrypoint scripts to gracefully
handle port conflicts. When a preferred port is busy, the system automatically
finds an alternative available port.

Changes:
- Add findFreePort() and findFreePorts() utilities to dev-safe.mjs
- Add buildSafeDevConfigAsync() for async config with dynamic allocation
- Update dev-with-automation.mjs to use async buildConfig with dynamic ports
- Update dev-static.mjs to use async buildConfig
- Add strictPort: true to vite.config.ts to fail-fast on conflicts
- Update tests for async buildConfig

The utilities try the preferred/default ports first, falling back to
OS-assigned ports only when needed. This preserves predictable defaults
while gracefully handling port conflicts.

Closes #222

* fix: address review feedback - add max retry, document race condition, improve tests

- Add max retry count (100 attempts) to port allocation loop to prevent
  infinite loops
- Fix findFreePort to handle preferredPort=0 correctly by skipping the
  port check and going straight to OS assignment
- Document race condition limitation in findFreePort JSDoc (accepted
  limitation with guidance on handling EADDRINUSE)
- Clarify JSDoc for buildSafeDevConfig vs buildSafeDevConfigAsync with
  clear guidance on when to use each
- Remove misleading 'must be after prereq check' comment
- Add comprehensive tests for findFreePort, findFreePorts, and
  buildSafeDevConfigAsync using actual port blocking
- Improve buildConfig tests with port uniqueness verification and
  fallback tests using high ports
- Use high ports (19xxx range) in tests to avoid conflicts with
  system services

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-09 14:46:10 -04:00
Rohit Malhotra 9098d9e6df feat: auto-generate random API keys for dev server authentication (#203) 2026-05-08 22:48:58 -04:00
Rohit Malhotraandopenhands 0b45d8c879 chore: default to released PyPI versions instead of git branches (#194)
- Change agent-server SDK default from git main to PyPI 1.21.1
- Change automation default from git main to PyPI 1.0.0a1
- Pin all SDK packages (agent-server, tools, workspace) to same version
- Keep ability to override with OH_AGENT_SERVER_GIT_REF/OH_AUTOMATION_GIT_REF
- Update tests and AGENTS.md documentation

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-08 15:08:01 -04:00
Rohit Malhotraandopenhands 7ebb116475 chore: update automation entrypoint to openhands.automation namespace (#191)
The automation package has been restructured to use the openhands.automation
namespace instead of the root automation namespace. This change updates the
uvicorn entrypoint from 'automation.app:app' to 'openhands.automation.app:app'.

Related: OpenHands/automation#101

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-08 14:23:50 -04:00
Rohit Malhotraandopenhands 6d1f0a74d9 feat: seed automation API key into agent-server secrets (#160)
* feat: seed automation API key into agent-server secrets

- Add seedAutomationSecret() that calls PUT /api/settings/secrets after
  agent-server is ready, storing the automation API key as
  OPENHANDS_AUTOMATION_API_KEY
- This makes the key available to agents during conversations so they can
  authenticate with the automation backend
- Add sessionApiKey to config for optional auth header
- Update help text and documentation

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

* test: add tests for seed automation secret and fix CI failure

- Add tests for localApiKey and sessionApiKey config in buildConfig
- Add tests for secrets documentation in help output
- Fix root-layout-refetch.test.tsx unhandled rejection from framer-motion
  by adding async cleanup with microtask flush

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

* fix: detect SESSION_API_KEY when seeding automation secret

The seedAutomationSecret() function was failing with 401 Unauthorized
because it wasn't detecting the SESSION_API_KEY environment variable
that the agent-server uses by default (V0 config).

The agent-server checks these env vars for session API keys:
- SESSION_API_KEY (V0 config, picked up by default factory)
- OH_SESSION_API_KEYS_0 (V1 config)

The original code only checked OH_SESSION_API_KEY and VITE_SESSION_API_KEY,
missing the actual env vars the server reads. In OpenHands Cloud
environments, SESSION_API_KEY is set automatically, causing the 401.

This fix adds SESSION_API_KEY and OH_SESSION_API_KEYS_0 to the
fallback chain, with SESSION_API_KEY taking highest precedence
since it matches the agent-server's default behavior.

Adds tests verifying:
- SESSION_API_KEY detection
- OH_SESSION_API_KEYS_0 detection
- Precedence order (SESSION_API_KEY > OH_SESSION_API_KEYS_0 > others)

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

* fix: add retry logic and longer timeout for secret seeding

On slower systems, the agent-server may take longer to start up,
causing the secret seeding to fail with 'fetch failed' errors.

This fix adds:
1. Increased initial wait timeout from 30s to 60s for agent-server startup
2. Retry logic in seedAutomationSecret (5 retries with 2s delay)
3. Better error logging showing elapsed time and last error
4. AbortSignal.timeout on fetch requests to avoid hanging
5. Skip seeding if server fails to start (with warning message)

The retry logic handles transient failures during server warmup
but immediately fails on 401/403 auth errors (no point retrying).

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-07 17:29:46 -04:00
6ebe7b4e7e feat: add automations frontend on /automations subpath (#141)
* feat: add automations frontend on /automations subpath

Port automations frontend from automation-repo to agent-canvas.

## Changes

### New Routes
- /automations - List view of all automations
- /automations/:automationId - Automation detail view

### New Components
- Automation list components: card, card-skeleton, group, empty-state, error-state
- Automation detail components: header, sections (config, prompt, plugins, activity)
- Shared UI components: toggle-switch, metadata-chip, status-badge, kebab-menu, search-input

### API Integration
- automation-service.api.ts - API client for automation CRUD operations
- Uses existing openHands axios client (shared base URL with agent server)

### MSW Mock Server Handlers
- automation-handlers.ts - Mock handlers for testing
- automations.mock.ts - Sample automation data
- automation-runs.mock.ts - Sample automation run data

### Tests
- API tests: automation-service.test.ts, automation-handlers.test.ts
- Component tests: toggle-switch, metadata-chip, search-input, error-state
- Detail component tests: section-card, run-status-badge, not-found-state

### Hooks
- use-automations.ts - React Query hook for fetching automations list
- use-automation-detail.ts - React Query hook for fetching single automation
- use-has-permission.ts - Permission checking utility hook

### Types
- automation.ts - TypeScript types for automation entities

### Icons
- Added SVG icons: activity, bell, calendar, check-circle, chevron-down,
  chevron-left, clock, cog, database, exclamation-circle, git-branch,
  kebab-vertical, power, puzzle, search, sparkle, target, trash, x-circle, x-mark

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

* feat: add local API key auth for automation backend

- Use VITE_AUTOMATION_API_KEY env var for frontend to authenticate
- Pass AUTOMATION_LOCAL_API_KEY to automation backend in dev mode
- Use dedicated axios instance with Bearer auth interceptor
- Add --refresh to uvx to ensure latest git commits are fetched
- URL-encode automation IDs in API paths

The default local API key is 'openhands-local-api-key' which matches
between the frontend and backend for local development.

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

* fix: automation service tests and ingress port conflicts

- Fix automation-service.test.ts to mock axios instance correctly
  (was mocking openHands but service uses automationAxios)
- Use vi.hoisted() for mock functions available during vi.mock hoisting
- Change ingress test ports from 19000-19003 to 29000-29003 to avoid
  conflict with VS Code server on port 19000

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

* fix: add CORS origins for automation backend in dev mode

The automation backend defaults CORS origins to app.all-hands.dev,
which blocks localhost requests. Add localhost origins for the
ingress port and Vite dev server port.

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

* fix: update create-instructions styling and add missing i18n keys

- Use semantic color tokens (text-content, text-basic, bg-base-secondary,
  bg-base, border-default) instead of hardcoded neutral-* colors
- Add all AUTOMATIONS$ i18n keys for the automations frontend

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-05-08 01:49:13 +07:00
Hiep Le dd744b13f6 feat: cloud backend support with multi-backend selector and SaaS proxy routing (#145)
* feat: multi-backend support with cloud SaaS proxy routing

* feat: route conversation export through cloud proxy on cloud backends

* fix: route conversation delete through cloud proxy on cloud backends

* fix: forward settings diffs verbatim through cloud proxy save

* fix: surface cloud-aware settings sub-pages and gate local-only routes

* fix: route secrets settings through cloud proxy on cloud backends

* fix: route conversation stop runtime through cloud proxy on cloud backends

* fix: re-expose planning agent UI for cloud backends and route plan file reads through cloud proxy

* fix: route Display Cost runtime fetch through cloud proxy and ungate local metrics without session API key

* fix: handle WAITING_FOR_SANDBOX task status from cloud backends to prevent UI crash

* fix: re-expose Public Share in conversation menu for cloud backends

* fix: redirect to home when switching backends from a conversation page

* fix: hide cloud orgs the API key can't access in backend selector

* feat: support running multiple local agent-servers with shared persistence

* fix: lint

* fix: failing tests
2026-05-08 01:17:40 +07:00
988cfed5c6 feat: add automation backend integration with standalone ingress proxy (#127)
* feat: add automation backend integration with standalone ingress proxy

- Add scripts/ingress.mjs: standalone HTTP reverse proxy for routing traffic
  to multiple backends based on URL path prefix
- Add scripts/dev-with-automation.mjs: orchestrates full stack with
  agent-server, automation backend (both via uvx), Vite, and ingress
- Make 'npm run dev' run full stack by default (was dev:safe, now dev:automation)
- Rename 'npm run dev:safe' to 'npm run dev:minimal' for agent-server + Vite only
- Update README with new quickstart showing full stack as default
- Update AGENTS.md with architecture documentation

Architecture:
  http://localhost:8000 (Ingress)
  ├── /api/automation/* → Automation Backend (:18001)
  ├── /api/*, /sockets  → Agent Server (:18000)
  └── /* (default)      → Vite Dev Server (:3001)

* test: add tests for ingress and dev-with-automation scripts

- Add __tests__/scripts/ingress.test.ts with 14 tests covering:
  - CLI argument parsing (--help, --port, --route, --default)
  - Route matching (exact, prefix, longest-match-first)
  - Proxy functionality (forwarding, query params, error handling)
  - 502 response when backend unavailable
  - 503 response for unmatched routes with no default

- Add __tests__/scripts/dev-with-automation.test.ts with 19 tests covering:
  - buildAutomationCommand() with various git refs/repos
  - buildConfig() port and path configuration
  - CLI --help output
  - Graceful exit when uvx is missing

- Export testable functions from dev-with-automation.mjs

* fix: prevent dev-with-automation from auto-executing when imported

The script was calling main() unconditionally, which caused test failures
when vitest imported the module. Now check if the module is the main entry
point before executing.

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-07 12:02:15 -04:00
Graham Neubigandopenhands 62871d3a7b rename agent-server-gui to agent-canvas (#121)
- npm package: @openhands/agent-server-gui -> @openhands/agent-canvas
- README/DEVELOPMENT/AGENTS/codereview guide updated to new name
- GitHub URL in onboarding screen + tests updated
- dev launcher env vars renamed: OH_GUI_SAFE_* -> OH_CANVAS_SAFE_*
- default state dir: ~/.openhands/agent-server-gui -> ~/.openhands/agent-canvas
- i18n strings replace 'GUI' phrasing with 'Agent Canvas' across settings,
  upgrade, onboarding, and unavailable copy
- Test fixtures, working-dir paths, and library-consumer smoke updated

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-06 13:07:01 -04:00
Rohit Malhotraandopenhands cccecf1100 Fix uvx command to use --from syntax for PyPI packages (#122)
The openhands-agent-server package exposes an executable named
'agent-server', not 'openhands-agent-server'. When using PyPI versions
(either specific or latest), we need to use the --from syntax:
  uvx --from openhands-agent-server agent-server

This fixes the error:
  An executable named 'openhands-agent-server' is not provided by
  package 'openhands-agent-server'.
  Use 'uvx --from openhands-agent-server agent-server' instead.

Fixes #117

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-06 12:07:41 -04:00