Commit Graph
20 Commits
Author SHA1 Message Date
e0b115757b fix: route runtime services through server_info (#16090)
Co-authored-by: neubig <398875+neubig@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: neubig <neubig@users.noreply.github.com>
2026-08-02 22:39:43 -04:00
Rohit Malhotraandopenhands 22fe594133 feat: forward automation telemetry context (#1917)
* feat: forward telemetry context to automations

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

* feat: sync automation telemetry consent

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

* fix: default automation telemetry key in launchers

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

* fix: bake production telemetry defaults into npm package

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

* fix: dedupe automation consent sync

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

* chore: bump automation version to 1.3.0

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-24 05:47:26 +00:00
Vasco Schiavo 2d7a3985b3 fix: spawn dev services without a shell on Windows (#1859)
On Windows, spawnService ran uvx through cmd.exe (shell: true), so the `<`
in the `agent-client-protocol<0.11` version constraint was parsed as input
redirection and agent-server exited immediately with "The system cannot find
the file specified."

Resolve the command to its absolute path with where.exe and spawn it directly,
with no shell, so argument metacharacters stay literal. npm is unaffected: it is
already wrapped in cmd.exe by buildNpmScriptCommand before it reaches
spawnService.
2026-07-20 11:00:13 +02:00
519c856c37 feat: support serving Canvas under a subpath (#1796)
* feat: support serving canvas under subpath

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

* fix: redirect root app routes to canvas base path

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-07-15 12:24:11 -04:00
Rohit Malhotraandopenhands b1ece3d1d3 fix: use dual-stack (::) binding for static-server to fix Docker e2e connection errors (#1104)
* fix: use dual-stack (::) binding for static-server to fix Docker e2e connection errors

The Docker e2e tests suffered frequent ECONNREFUSED errors because
static-server.mjs defaulted to 0.0.0.0 (IPv4-only), while localhost
can resolve to ::1 (IPv6) on CI runners. Meanwhile, ingress.mjs (used
by the npm path) already bound to :: (dual-stack) and never had this
problem.

Changes:
- static-server.mjs: default host from 0.0.0.0 → :: (dual-stack)
- docker/entrypoint.sh: --host 0.0.0.0 → --host :: for both
  static-server instances
- playwright.mock-llm-docker.config.ts: switch URLs from 127.0.0.1 to
  localhost (now safe since the server accepts both IPv4 and IPv6)
- playwright.mock-llm.config.ts: drop explicit --host 0.0.0.0 from
  public-mode server (inherits the new :: default)
- dev-static.mjs, dev-with-automation.mjs: drop explicit --host 0.0.0.0
  (inherits the new :: default)
- AGENTS.md: replace IPv4-only guidance with dual-stack documentation

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

* fix: skip partial-stack/cross-connect tests when build/ is absent (Docker e2e)

The partial-stack and cross-connect tests spawn bin/agent-canvas.mjs
locally, which requires a pre-built build/ directory. In the Docker e2e
workflow there is no host-side build — the frontend lives inside the
Docker image. These tests are already covered by the npm e2e workflow.

Convert the hard expect(existsSync(...)).toBe(true) assertions to
test.skip() so they are gracefully skipped instead of failing.

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

* fix: add test.skip to port-conflict test for missing build dir

The port-conflict test also spawns bin/agent-canvas.mjs --frontend-only,
which fails before reaching the port conflict when no build/ exists.

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

* fix: handle EPIPE/socket errors in static-server proxy to prevent crashes

The static-server reverse proxy crashed with an unhandled 'error' event
(EPIPE) when a client disconnected mid-response — e.g. during browser
navigation or health-check probes. This killed the entire process and
caused cascading ECONNREFUSED in subsequent Docker e2e tests.

Add error handlers on all piped sockets (req, res, proxySocket, socket)
so write errors from client disconnects are absorbed instead of crashing
the server process.

Also add test.skip for the port-conflict test when build/ is missing.

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-03 20:54:52 +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
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
59058c7609 fix(ui): left navigation rail, mobile drawer, and responsive chrome (#623)
* fix: archived row icon, default-local config sync, and dockerless dev fixes

Archived MISSING sandboxes show an archive icon in the status column instead of a gray dot and pill; ERROR sandboxes keep the error pill. Harden port checks and automation CORS for alternate frontend ports, set agent-server HOME to the host home on macOS, sync legacy agent-server config when editing the default-local backend, and clarify static stack rebuild/skip-build behavior.

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

* fix(ui): sidebar conversation list flush to rail with stable gutter

Drop expanded aside `md:pr-0` so the thread scrollbar aligns with the rail
while nav, logo row, and footer keep horizontal padding. Use scrollbar-gutter
on the conversation list for consistent right inset with or without overflow.

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

* fix(ui): 40px backend selector and non-italic combobox text

Pin the backend Dropdown trigger to h-10, add an italicPlaceholder escape
hatch, and force upright type for value/placeholder in the selector. Add a
subtle top border above Add Workspace in the new-conversation menu.

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

* fix(ui): full-bleed sidebar footer divider to left rail

Pull the backend block border past aside `pl-2` with matching width calc;
keep inner `px-2` so controls stay aligned with nav.

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

* fix(ui): align context menus with sidebar filter menu and muted row icons

Match list padding, border, and shadow to the conversations filter surface;
use theme foreground/muted tokens; reserve leading icons as muted until row
hover/focus. Simplify list-item rows and remove unused context-menu height
constant.

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

* fix(ui): unify menu dividers, section labels, and filter actions

Standardize full-bleed menu dividers via Divider inset="menu", give filter
menu section headings consistent pt-1 padding, and add icons to hide/show
and delete-all rows in the conversation panel filter menu.

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

* fix(ui): rename sidebar conversations link to New Chat

The /conversations nav entry should read "New Chat" instead of "Code" to match user expectations for starting a conversation.

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

* fix(ui): polish cloud new-thread popover search and repo list

Use a flat search row with icon and menu divider, align horizontal padding with list items, and show a custom scrollbar on the repository list.

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

* fix(ui): unify filter menu section headings and action icons

Share pt-1 section label padding via MenuHeading, fold hide/delete rows into
MenuRow with Eye and Trash icons, and add a regression test for both actions.

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

* fix(ui): add settings tooltip and neutral delete-all row

Show a white hover tooltip on the backend selector settings button and
style Delete all like other filter menu actions instead of danger red.

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

* fix(ui): theme scrollbars/skeletons and improve settings tooltips

Derive scrollbar colors from the cool-grey scale, use interactive-active
for skeleton loaders, and fix settings tooltip placement when the drawer
is open plus collapsed-sidebar coverage.

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

* feat(ui): replace mobile top nav with left drawer

On small screens, hide the horizontal sidebar strip and open the full
vertical nav from a top-bar chevron toggle, matching the desktop collapse
control icon.

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

* fix(ui): restore mobile page gutters and center home title

Apply 14px horizontal padding in the root outlet on mobile, keep
conversation full-bleed, and center the home heading with w-full text-center.

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

* fix(ui): correct mobile nav icons and home title line height

Use PanelLeft for the mobile menu trigger, ChevronLeft to close the
drawer, and relax the home headline leading when it wraps.

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

* fix(ui): remove border under mobile nav menu bar

The top-bar menu trigger no longer draws a divider above page content.

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

* feat(ui): mobile settings/customize hubs and animated nav drawer

On mobile, Settings and Customize open list hubs matching desktop left nav;
detail pages show drawer and back controls in the top bar. The slide-out nav
drawer now animates open and closed with a fading scrim.

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

* fix(ui): keep sidebar icons aligned when collapsing the rail

Use shared h-10 row geometry and a fixed icon column so collapsed and
expanded states share padding and gap; labels clip instead of recentering.

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

* fix(ui): stabilize collapsed sidebar icon column and hover targets

Use a shared 18px icon slot in both rail states, square collapsed
controls for hover/active, and symmetric rail padding so icons and the
logo stay aligned without full-width row highlights.

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

* fix(ui): refine collapsed sidebar slots and empty-list load-more

Use consistent px-2.5 rail padding with 40×40 collapsed icon slots via
SidebarCollapsedIconSlot, fix backend dot anchoring and logo alignment,
and hide “Load more” when no conversations are visible after filtering.

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

* fix(ui): keep conversations header on one line during sidebar resize

Use flex-nowrap with a truncating title so the toolbar row does not wrap
while the drawer animates, and nudge the collapsed backend status dot up-left.

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

* fix(ui): align DropdownMenu padding and tighten backend footer actions

Use uniform `p-1` on the combobox menu panel to match other menus, and
remove vertical gap between Add/Manage backend items in the selector footer.

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

* fix(ui): trim vertical padding on cloud repo search row

Drop wrapper `py-1` so the new-conversation repo search aligns with the
dropdown chrome; horizontal inset stays `px-2`.

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

* fix(ui): flush cloud repo menu divider against search and list

Drop flex `gap-1` on the popover so the rule sits tight to the search row
and repository list; keep tab stripe spacing with `py-1` on the provider row.

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

* fix(ui): show backend settings tooltip above when sidebar is expanded

Use `useSidebarCollapsed()` so the gear tooltip uses top placement on the
full-width rail while keeping left placement for the icon-only strip unless
the conversation right drawer is open.

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

* fix(ui): use full-screen route for mobile tools panel and tidy conversation chrome

Restore horizontal inset on the conversation route, fold the sidebar opener into the chat header, and open Files/Tools via `/conversations/:id/panel` with a dedicated back affordance instead of a bottom sheet.

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

* fix(ui): only show sidebar drawer toggle when the rail is hidden

The hamburger used the 1024px layout breakpoint while the sidebar stays visible from the `md` rail width up, so it duplicated chrome between tablet widths. Gate the toggle and header padding on the same max-md width as the rail (<=767px). Mirror the right-panel icon horizontally for the right-side drawer.

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

* fix(ui): align mobile conversation chrome, chat padding, and settings scroll

Unify mobile top-bar icon buttons with shared classes; add compact conversation
tabs and consistent horizontal padding for chat plus stable composer/git chrome.
Move settings and extensions horizontal inset into layout helpers and drop the
root outlet gutter so pages control their own padding.

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

* fix(ui): home mobile padding and Lucide sizes in chat mobile header

Add px-4 to the home shell on small viewports and shift launcher inset to
md+ only. Pin PanelLeft/ChevronLeft to 20px to match the right-panel icon and
restore chat header left inset when the rail menu toggle shows.

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

* fix(ui): use block-drawer icon for mobile sidebar toggle

Match the right-panel control’s SVG so both header icons share the same
optical weight instead of Lucide PanelLeft filling the hit target.

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

* refactor: remove unrelated file

* refactor: remove unrelated files

* fix: failing tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-21 02:18:19 +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 4db59b8b94 Proxy agent-server FastAPI docs (/docs, /redoc, /openapi.json) through the ingress (#501)
* feat(ingress): route /docs to the agent server

Add /docs to the list of prefixes proxied to the agent-server in:
  - vite.config.ts (Vite dev server proxy)
  - scripts/dev-with-automation.mjs (ingress + static-server fallback)
  - scripts/dev-static.mjs (ingress + static-server fallback)

Update the explanatory comment in scripts/static-server.mjs to match.

This exposes the agent-server's FastAPI Swagger UI at `/docs` on the
ingress port, alongside the automation backend's existing
`/api/automation/docs`.

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

* feat(ingress): also route /redoc and /openapi.json to the agent server

Without /openapi.json, the Swagger UI page served at /docs (added in the
previous commit) renders but fails to load any spec. /redoc is the
FastAPI-served ReDoc alternative and benefits from the same fix.

Routes are added everywhere /docs already is:
  - vite.config.ts (Vite dev server proxy)
  - scripts/dev-with-automation.mjs (ingress + static-server fallback)
  - scripts/dev-static.mjs (ingress + static-server fallback)

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

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 18:46:13 -06: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 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
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
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
Graham Neubig ffd19e977f Fix Windows dev script startup (#199)
* Fix Windows dev script startup

* Run CI on Windows

* Disable npm cache on Windows CI
2026-05-08 21:36:20 -04:00
Robert Brennanandopenhands 9c3b936d16 Add npm run dev:static for offline / high-latency development (#168)
Mirrors the dev:automation backend stack (agent-server + automation +
ingress) but serves a production frontend build through a small static
server instead of Vite. Designed for use over flaky / high-RTT links
where Vite's ~1000 ESM module fetches make full reloads painfully slow:
hashed assets are now sent with public/immutable cache headers, so an
SPA reload is ~1 round-trip (304 on index.html) and zero asset fetches.

scripts/static-server.mjs: combined static-file server + reverse proxy.
A drop-in for sirv-cli that additionally proxies the same prefixes Vite
proxies in dev (/api, /api/automation, /sockets, /server_info, /alive,
/health, /ready) so hitting :3001 directly behaves like Vite's dev
server — without it, sirv-cli's --single fallback turns /server_info
into the SPA shell whenever a tunnel exposes the static port instead of
the ingress port. Caches /assets/* immutable, index.html no-cache,
weak ETags.

scripts/dev-static.mjs: orchestrator that builds the frontend, then
spawns agent-server, automation, static-server, and the existing
ingress with the same route table as dev-with-automation.

scripts/dev-safe.mjs: add isPortBusy() and
releaseStaleConversationLeases() helpers. The agent-server tags each
conversation directory with an owner_lease.json keyed to a per-process
owner_instance_id (45 s TTL, heartbeat-renewed) and skip-loads any
conversation whose lease is held by a different instance. If the
previous agent-server died ungracefully — or you restart inside the
TTL window — every existing conversation becomes invisible to the new
instance until the leases age out. dev:static now port-checks for a
live agent-server (aborts on conflict), then unlinks stale leases so
conversations created by npm run dev are immediately visible.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-08 17:29:27 +07:00