* 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>
* 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>
* 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>
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>
* 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>
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>
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>
* 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>
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>
* 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
* 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>
* 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>
* 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>
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>
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>
* 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>
- 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>
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>
* 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>
* 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>
* 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
* 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>
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>
* feat: use agent server APIs for settings persistence
- Replace localStorage with HTTP API for settings storage
- Use `X-Expose-Secrets: encrypted` header for GET /api/settings
to receive encrypted secrets (not exposing raw values)
- Use `secrets_encrypted: true` in start conversation payload
- Add `getSettingsForConversation()` to build encrypted settings
payload for conversation start endpoint
- Update secrets service to use /api/settings/secrets endpoints
- Add mock handlers for settings and secrets API endpoints
- Update tests for new API-based settings flow
This integrates with software-agent-sdk PR #3060
(feat/encrypted-secrets-in-transit) which adds server-side
encryption support for secrets in transit.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update test mocks for encrypted settings API and add OH_SECRET_KEY support
- Update use-create-conversation-metadata.test.ts to mock getSettingsForConversation()
which is now called by buildStartConversationRequestWithEncryptedSettings
- Skip flaky onOpen websocket test that times out intermittently in CI
- Add OH_SECRET_KEY environment variable support in dev-safe.mjs:
- Uses default key for local development
- Can be overridden via OH_SECRET_KEY environment variable
- Logs secret key source at startup
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md for settings API and OH_SECRET_KEY
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update secrets service to use agent-server API routes
Changes:
- Update SecretsService to use /api/settings/secrets endpoints instead of /api/v1/secrets
- Simplify secrets-service.types.ts to remove unused pagination types
- Update use-get-secrets hook to do client-side filtering (agent-server doesn't support pagination)
- Update mock handlers to only use agent-server API routes
- Update secrets-settings test to mock getSecrets instead of searchSecrets
- Remove pageSize option from useSearchSecrets since agent-server doesn't paginate
The agent-server API routes (per SDK PR #3060):
- GET /api/settings/secrets - List secrets (names/descriptions only)
- GET /api/settings/secrets/{name} - Get secret value
- PUT /api/settings/secrets - Upsert secret
- DELETE /api/settings/secrets/{name} - Delete secret
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md for secrets API routes
- Document the agent-server secrets CRUD routes in MSW handlers list
- Update git provider token persistence note to reflect server-side storage
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update secret name validation to match agent-server requirements
- Change pattern from '^\S*$' (no whitespace) to '^[a-zA-Z][a-zA-Z0-9_]{0,63}$'
- Add title prop to SettingsInput component for validation error messages
- Secret names must: start with letter, contain only letters/numbers/underscores, be 1-64 chars
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: include custom secrets in conversation requests via LookupSecret
Custom secrets configured in Settings > Secrets are now automatically
included in conversation start requests. Instead of exposing secret values
to the frontend, we use LookupSecret entries that point to the agent-server
endpoint /api/settings/secrets/{name}. The agent-server fetches the actual
values at runtime.
Changes:
- Add LookupSecret interface to agent-server-adapter.ts
- Add customSecrets option to StartConversationOptions
- Build LookupSecret entries for each custom secret in buildStartConversationRequest
- Update buildStartConversationRequestWithEncryptedSettings to fetch and include
custom secrets list from SecretsService.getSecrets()
- Include X-Session-API-Key header in LookupSecret when configured
This ensures secrets never touch the frontend in plaintext while still
making them available to conversations.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address review comments - no localStorage fallback, retry logic, SDK docs
Review feedback addressed:
1. secrets-service.ts: Server storage MUST succeed before updating localStorage
- addGitProvider now stores to server FIRST, only updates localStorage on success
- createSecret/updateSecret/deleteSecret now throw on failure (no silent returns)
- Added retry logic with exponential backoff for all API calls
2. settings-service.api.ts: No silent fallback for encrypted settings
- getSettingsForConversation now throws if encrypted fetch fails
- Conversations should not start with broken/redacted credentials
- Added retry logic with exponential backoff
3. AGENTS.md: Document SDK dependency
- Settings persistence APIs require SDK PR #3060
- Until released, npm run dev defaults to main branch
- Documented git provider storage design (server + localStorage)
4. dev-safe.mjs: Default to SDK main branch
- Added DEFAULT_GIT_REF='main' constant
- npm run dev now uses main until settings APIs are released
- TODO comment to update once released
Note: Git provider tokens still use localStorage for frontend git API calls
(repo search, branches), but MUST succeed on server first.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: update server secret when only host changes
When updating just the host (empty token), the server secret's description
must also be updated to keep metadata in sync. Previously, only localStorage
was updated, violating the 'server storage must succeed first' principle.
Now the host-only update path also calls createSecret() to update the
server secret's description before updating localStorage.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: use uvx for temporary agent-server installation in dev mode
- Replace direct agent-server CLI invocation with uvx temporary install
- Add OH_AGENT_SERVER_VERSION env var for specific PyPI versions
- Add OH_AGENT_SERVER_GIT_REF env var for git commits/branches
- Auto-install uv in .openhands/setup.sh if not present
- Update documentation (README, DEVELOPMENT.md, AGENTS.md)
- Add comprehensive tests for buildAgentServerCommand()
This removes the requirement to permanently install agent-server via
'uv tool install'. Users only need uv installed, and npm run dev will
automatically download and run the appropriate agent-server version.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: use subdirectory syntax for git ref in uvx monorepo
The software-agent-sdk is a uv workspace monorepo with packages in
subdirectories (openhands-agent-server/, openhands-tools/, etc.).
When installing from git, uvx requires the #subdirectory= fragment to
specify which package to install from the workspace.
Tested with: OH_AGENT_SERVER_GIT_REF=main npm run dev
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
Add actionable dev-safe output when agent-server is missing, including README and uv installation guidance, and update tests plus quickstart docs.
Co-authored-by: openhands <openhands@all-hands.dev>